diff --git a/library/common/src/main/java/com/google/android/exoplayer2/util/MimeTypes.java b/library/common/src/main/java/com/google/android/exoplayer2/util/MimeTypes.java index e61ab83777..5154e309f5 100644 --- a/library/common/src/main/java/com/google/android/exoplayer2/util/MimeTypes.java +++ b/library/common/src/main/java/com/google/android/exoplayer2/util/MimeTypes.java @@ -97,6 +97,7 @@ public final class MimeTypes { public static final String APPLICATION_DVBSUBS = BASE_TYPE_APPLICATION + "/dvbsubs"; public static final String APPLICATION_EXIF = BASE_TYPE_APPLICATION + "/x-exif"; public static final String APPLICATION_ICY = BASE_TYPE_APPLICATION + "/x-icy"; + public static final String APPLICATION_AIT = BASE_TYPE_APPLICATION + "/ait"; private static final ArrayList customMimeTypes = new ArrayList<>(); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/MetadataDecoderFactory.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/MetadataDecoderFactory.java index 0b653830a3..a9c124eedb 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/metadata/MetadataDecoderFactory.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/metadata/MetadataDecoderFactory.java @@ -17,6 +17,7 @@ package com.google.android.exoplayer2.metadata; import androidx.annotation.Nullable; import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.metadata.dvbsi.AitDecoder; import com.google.android.exoplayer2.metadata.emsg.EventMessageDecoder; import com.google.android.exoplayer2.metadata.icy.IcyDecoder; import com.google.android.exoplayer2.metadata.id3.Id3Decoder; @@ -67,7 +68,8 @@ public interface MetadataDecoderFactory { return MimeTypes.APPLICATION_ID3.equals(mimeType) || MimeTypes.APPLICATION_EMSG.equals(mimeType) || MimeTypes.APPLICATION_SCTE35.equals(mimeType) - || MimeTypes.APPLICATION_ICY.equals(mimeType); + || MimeTypes.APPLICATION_ICY.equals(mimeType) + || MimeTypes.APPLICATION_AIT.equals(mimeType); } @Override @@ -83,6 +85,8 @@ public interface MetadataDecoderFactory { return new SpliceInfoDecoder(); case MimeTypes.APPLICATION_ICY: return new IcyDecoder(); + case MimeTypes.APPLICATION_AIT: + return new AitDecoder(); default: break; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/dvbsi/Ait.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/dvbsi/Ait.java new file mode 100644 index 0000000000..4aa780aba1 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/metadata/dvbsi/Ait.java @@ -0,0 +1,58 @@ +package com.google.android.exoplayer2.metadata.dvbsi; + +import android.os.Parcel; +import android.os.Parcelable; + +import com.google.android.exoplayer2.metadata.Metadata; + +public class Ait implements Metadata.Entry { + /* + The application shall be started when the service is selected, unless the + application is already running. + */ + public static final int CONTROL_CODE_AUTOSTART = 0x01; + /* + The application is allowed to run while the service is selected, however it + shall not start automatically when the service becomes selected. + */ + public static final int CONTROL_CODE_PRESENT = 0x02; + + public final int controlCode; + public final String url; + + Ait(int controlCode, String url) { + this.controlCode = controlCode; + this.url = url; + } + + @Override + public String toString() { + return "Ait(controlCode = " + controlCode + ", url = " + url + ")"; + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel parcel, int i) { + parcel.writeString(url); + parcel.writeInt(controlCode); + } + + public static final Parcelable.Creator CREATOR = + new Parcelable.Creator() { + @Override + public Ait createFromParcel(Parcel in) { + String url = in.readString(); + int controlCode = in.readInt(); + return new Ait(controlCode, url); + } + + @Override + public Ait[] newArray(int size) { + return new Ait[size]; + } + }; +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/dvbsi/AitDecoder.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/dvbsi/AitDecoder.java new file mode 100644 index 0000000000..249ae0ce2f --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/metadata/dvbsi/AitDecoder.java @@ -0,0 +1,195 @@ +package com.google.android.exoplayer2.metadata.dvbsi; + +import com.google.android.exoplayer2.metadata.Metadata; +import com.google.android.exoplayer2.metadata.MetadataDecoder; +import com.google.android.exoplayer2.metadata.MetadataInputBuffer; +import com.google.android.exoplayer2.util.ParsableByteArray; +import com.google.android.exoplayer2.util.TimestampAdjuster; + +import java.nio.ByteBuffer; +import java.nio.charset.Charset; +import java.util.ArrayList; + +public class AitDecoder implements MetadataDecoder { + // Specification of AIT can be found in 5.3.4 of TS 102 809 v1.1.1 + // https://www.etsi.org/deliver/etsi_ts/102800_102899/102809/01.01.01_60/ts_102809v010101p.pdf + private final static int DESCRIPTOR_TRANSPORT_PROTOCOL = 0x02; + + private final static int DESCRIPTOR_SIMPLE_APPLICATION_LOCATION = 0x15; + + private final static int TRANSPORT_PROTOCOL_HTTP = 3; + + private TimestampAdjuster timestampAdjuster; + + private final ParsableByteArray sectionData; + + public AitDecoder() { + sectionData = new ParsableByteArray(); + } + + @Override + public Metadata decode(MetadataInputBuffer inputBuffer) { + if (timestampAdjuster == null + || inputBuffer.subsampleOffsetUs != timestampAdjuster.getTimestampOffsetUs()) { + timestampAdjuster = new TimestampAdjuster(inputBuffer.timeUs); + timestampAdjuster.adjustSampleTimestamp(inputBuffer.timeUs - inputBuffer.subsampleOffsetUs); + } + + ByteBuffer buffer = inputBuffer.data; + byte[] data = buffer.array(); + int size = buffer.limit(); + sectionData.reset(data, size); + + int tableId = sectionData.peekUnsignedByte(); + + //Only this table is allowed in AIT streams + if (tableId == 0x74) { + return parseAit(sectionData); + } + + return new Metadata(); + } + + private Metadata parseAit(ParsableByteArray sectionData) { + int tmp; + + int tableId = sectionData.readUnsignedByte(); + + tmp = sectionData.readUnsignedShort(); + int endOfSection = sectionData.getPosition() + (tmp & 4095) - 4 /* Ignore leading CRC */; + + tmp = sectionData.readUnsignedShort(); + int applicationType = tmp & 0x7fff; + + tmp = sectionData.readUnsignedByte(); + int versionNumber = (tmp & 0x3e) >> 1; + boolean current = (tmp & 1) == 1; + + int section_number = sectionData.readUnsignedByte(); + int last_section_number = sectionData.readUnsignedByte(); + + tmp = sectionData.readUnsignedShort(); + int commonDescriptorsLength = tmp & 4095; + + //Since we currently only keep url and control code, which are unique per application, + //there is no useful information in common descriptor. + sectionData.skipBytes(commonDescriptorsLength); + + tmp = sectionData.readUnsignedShort(); + int appLoopLength = tmp & 4095; + + ArrayList aits = new ArrayList<>(); + while(sectionData.getPosition() < endOfSection) { + // Values that will be stored in Ait() + String aitUrlBase = null; + String aitUrlExtension = null; + int aitControlCode = -1; + + long application_identifier = sectionData.readUnsignedInt24() << 24L; + application_identifier |= sectionData.readUnsignedInt24(); + int controlCode = sectionData.readUnsignedByte(); + + aitControlCode = controlCode; + + tmp = sectionData.readUnsignedShort(); + int sectionLength = tmp & 4095; + int positionOfNextSection = sectionData.getPosition() + sectionLength; + while(sectionData.getPosition() < positionOfNextSection) { + int type = sectionData.readUnsignedByte(); + int l = sectionData.readUnsignedByte(); + int positionOfNextSection2 = sectionData.getPosition() + l; + + if(type == DESCRIPTOR_TRANSPORT_PROTOCOL) { + int protocolId = sectionData.readUnsignedShort(); + int label = sectionData.readUnsignedByte(); + + if(protocolId == TRANSPORT_PROTOCOL_HTTP) { + while (sectionData.getPosition() < positionOfNextSection2) { + int urlBaseLength = sectionData.readUnsignedByte(); + String urlBase = sectionData.readString(urlBaseLength); + int extensionCount = sectionData.readUnsignedByte(); + aitUrlBase = urlBase; + for (int i = 0; i < extensionCount; i++) { + int len = sectionData.readUnsignedByte(); + sectionData.skipBytes(len); + } + } + } + } else if(type == DESCRIPTOR_SIMPLE_APPLICATION_LOCATION) { + String url = sectionData.readString(l); + aitUrlExtension = url; + } + + sectionData.setPosition(positionOfNextSection2); + } + + sectionData.setPosition(positionOfNextSection); + + if(aitControlCode != -1 && aitUrlBase != null && aitUrlExtension != null) { + aits.add(new Ait(aitControlCode, aitUrlBase + aitUrlExtension)); + } + } + + return new Metadata(aits); + } + + static final String dvbCharset[] = { + "ISO-8859-15", + "ISO-8859-5", + "ISO-8859-6", + "ISO-8859-7", + "ISO-8859-8", + "ISO-8859-9", + null, + "ISO-8859-11", + null, + "ISO-8859-13", + null, + "ISO-8859-15", + null, + null, + null, + null, + //0x10 + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + }; + + static String readDvbString(ParsableByteArray data, int length) { + if(length == 0) return null; + int charsetSelect = data.peekUnsignedByte(); + if(charsetSelect >= 0x20) + return data.readString(length, Charset.forName("ISO-8859-15")); + data.skipBytes(1); + if(charsetSelect == 0x1f) { + data.skipBytes(length - 1); + return null; + } + if(charsetSelect != 0x10) + return data.readString(length-1, Charset.forName(dvbCharset[charsetSelect])); + if(length == 2) { + data.skipBytes(1); + return null; + } + charsetSelect = data.readUnsignedShort(); + if(charsetSelect > 1 && charsetSelect < 0x10) { + String charsetName = "ISO-8859-" + charsetSelect; + return data.readString(length-3, Charset.forName(charsetName)); + } + return null; + } +} diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ts/DefaultTsPayloadReaderFactory.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ts/DefaultTsPayloadReaderFactory.java index ad1e8cc264..a22eb023dd 100644 --- a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ts/DefaultTsPayloadReaderFactory.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ts/DefaultTsPayloadReaderFactory.java @@ -18,11 +18,17 @@ package com.google.android.exoplayer2.extractor.ts; import android.util.SparseArray; import androidx.annotation.IntDef; import androidx.annotation.Nullable; + +import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.extractor.ExtractorOutput; +import com.google.android.exoplayer2.extractor.TrackOutput; import com.google.android.exoplayer2.extractor.ts.TsPayloadReader.EsInfo; import com.google.android.exoplayer2.util.CodecSpecificDataUtil; import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.ParsableByteArray; +import com.google.android.exoplayer2.util.TimestampAdjuster; + import java.lang.annotation.Documented; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -176,11 +182,41 @@ public final class DefaultTsPayloadReaderFactory implements TsPayloadReader.Fact case TsExtractor.TS_STREAM_TYPE_DVBSUBS: return new PesReader( new DvbSubtitleReader(esInfo.dvbSubtitleInfos)); + case TsExtractor.TS_STREAM_TYPE_AIT: + return new SectionReader(new SectionPassthrough(MimeTypes.APPLICATION_AIT)); default: return null; } } + public class SectionPassthrough implements SectionPayloadReader { + private TimestampAdjuster timestampAdjuster = null; + private final String mimeType; + private TrackOutput output; + + SectionPassthrough(String mimeType) { + this.mimeType = mimeType; + } + + @Override + public void init(TimestampAdjuster timestampAdjuster, ExtractorOutput extractorOutput, TsPayloadReader.TrackIdGenerator idGenerator) { + this.timestampAdjuster = timestampAdjuster; + idGenerator.generateNewId(); + output = extractorOutput.track(idGenerator.getTrackId(), C.TRACK_TYPE_METADATA); + output.format(Format.createSampleFormat(null, mimeType, + timestampAdjuster.getTimestampOffsetUs())); + } + + @Override + public void consume(ParsableByteArray sectionData) { + int sampleSize = sectionData.bytesLeft(); + output.sampleData(sectionData, sampleSize); + output.sampleMetadata(timestampAdjuster.getLastAdjustedTimestampUs(), C.BUFFER_FLAG_KEY_FRAME, + sampleSize, 0, null); + } + } + + /** * If {@link #FLAG_OVERRIDE_CAPTION_DESCRIPTORS} is set, returns a {@link SeiReader} for * {@link #closedCaptionFormats}. If unset, parses the PMT descriptor information and returns a diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ts/TsExtractor.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ts/TsExtractor.java index 35e8806a6f..b3f4e80f9f 100644 --- a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ts/TsExtractor.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ts/TsExtractor.java @@ -96,6 +96,9 @@ public final class TsExtractor implements Extractor { public static final int TS_STREAM_TYPE_SPLICE_INFO = 0x86; public static final int TS_STREAM_TYPE_DVBSUBS = 0x59; + //Those are special IDs, which don't have actual TS definitions + public static final int TS_STREAM_TYPE_AIT = 0x101; + public static final int TS_PACKET_SIZE = 188; public static final int TS_SYNC_BYTE = 0x47; // First byte of each TS packet. @@ -494,6 +497,7 @@ public final class TsExtractor implements Extractor { private static final int TS_PMT_DESC_REGISTRATION = 0x05; private static final int TS_PMT_DESC_ISO639_LANG = 0x0A; private static final int TS_PMT_DESC_AC3 = 0x6A; + private static final int TS_PMT_DESC_AIT = 0x6F; private static final int TS_PMT_DESC_EAC3 = 0x7A; private static final int TS_PMT_DESC_DTS = 0x7B; private static final int TS_PMT_DESC_DVB_EXT = 0x7F; @@ -578,7 +582,7 @@ public final class TsExtractor implements Extractor { pmtScratch.skipBits(4); // reserved int esInfoLength = pmtScratch.readBits(12); // ES_info_length. EsInfo esInfo = readEsInfo(sectionData, esInfoLength); - if (streamType == 0x06) { + if (streamType == 0x06 || streamType == 0x05) { streamType = esInfo.streamType; } remainingEntriesLength -= esInfoLength + 5; @@ -688,6 +692,8 @@ public final class TsExtractor implements Extractor { dvbSubtitleInfos.add(new DvbSubtitleInfo(dvbLanguage, dvbSubtitlingType, initializationData)); } + } else if (descriptorTag == TS_PMT_DESC_AIT) { + streamType = TS_STREAM_TYPE_AIT; } // Skip unused bytes of current descriptor. data.skipBytes(positionOfNextDescriptor - data.getPosition());