diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 4febfc7466..81ac490ae0 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -38,12 +38,16 @@ ([#8349](https://github.com/google/ExoPlayer/issues/8349)). * Add `DefaultHttpDataSource.Factory` and deprecate `DefaultHttpDataSourceFactory`. - * Populate codecs string for H.264/AVC in MP4, Matroska and FLV streams to - allow decoder capability checks based on codec profile/level - ([#8393](https://github.com/google/ExoPlayer/issues/8393)). * Add option to `MergingMediaSource` to clip the durations of all sources to have the same length ([#8422](https://github.com/google/ExoPlayer/issues/8422)). +* Extractors: + * Populate codecs string for H.264/AVC in MP4, Matroska and FLV streams to + allow decoder capability checks based on codec profile/level + ([#8393](https://github.com/google/ExoPlayer/issues/8393)). + * Populate codecs string for H.265/HEVC in MP4, Matroska and MPEG-TS + streams to allow decoder capability checks based on codec profile/level + ([#8393](https://github.com/google/ExoPlayer/issues/8393)). * Track selection: * Allow parallel adaptation for video and audio ([#5111](https://github.com/google/ExoPlayer/issues/5111)). diff --git a/library/common/src/main/java/com/google/android/exoplayer2/util/CodecSpecificDataUtil.java b/library/common/src/main/java/com/google/android/exoplayer2/util/CodecSpecificDataUtil.java index 3360e88d4f..0f8edb4acd 100644 --- a/library/common/src/main/java/com/google/android/exoplayer2/util/CodecSpecificDataUtil.java +++ b/library/common/src/main/java/com/google/android/exoplayer2/util/CodecSpecificDataUtil.java @@ -26,6 +26,8 @@ import java.util.List; public final class CodecSpecificDataUtil { private static final byte[] NAL_START_CODE = new byte[] {0, 0, 0, 1}; + private static final String[] HEVC_GENERAL_PROFILE_SPACE_STRINGS = + new String[] {"", "A", "B", "C"}; /** * Parses an ALAC AudioSpecificConfig (i.e. an 0 && constraintBytes[trailingZeroIndex - 1] == 0) { + trailingZeroIndex--; + } + for (int i = 0; i < trailingZeroIndex; i++) { + builder.append(String.format(".%02X", constraintBytes[i])); + } + return builder.toString(); + } + /** * Constructs a NAL unit consisting of the NAL start code followed by the specified data. * diff --git a/library/common/src/main/java/com/google/android/exoplayer2/video/HevcConfig.java b/library/common/src/main/java/com/google/android/exoplayer2/video/HevcConfig.java index 100a824a97..b80b1e6942 100644 --- a/library/common/src/main/java/com/google/android/exoplayer2/video/HevcConfig.java +++ b/library/common/src/main/java/com/google/android/exoplayer2/video/HevcConfig.java @@ -17,8 +17,10 @@ package com.google.android.exoplayer2.video; import androidx.annotation.Nullable; import com.google.android.exoplayer2.ParserException; +import com.google.android.exoplayer2.util.CodecSpecificDataUtil; import com.google.android.exoplayer2.util.NalUnitUtil; import com.google.android.exoplayer2.util.ParsableByteArray; +import com.google.android.exoplayer2.util.ParsableNalUnitBitArray; import java.util.Collections; import java.util.List; @@ -27,9 +29,6 @@ import java.util.List; */ public final class HevcConfig { - @Nullable public final List initializationData; - public final int nalUnitLengthFieldLength; - /** * Parses HEVC configuration data. * @@ -61,8 +60,9 @@ public final class HevcConfig { data.setPosition(csdStartPosition); byte[] buffer = new byte[csdLength]; int bufferPosition = 0; + @Nullable String codecs = null; for (int i = 0; i < numberOfArrays; i++) { - data.skipBytes(1); // completeness (1), nal_unit_type (7) + int nalUnitType = data.readUnsignedByte() & 0x7F; // completeness (1), nal_unit_type (7) int numberOfNalUnits = data.readUnsignedShort(); for (int j = 0; j < numberOfNalUnits; j++) { int nalUnitLength = data.readUnsignedShort(); @@ -71,21 +71,49 @@ public final class HevcConfig { bufferPosition += NalUnitUtil.NAL_START_CODE.length; System.arraycopy( data.getData(), data.getPosition(), buffer, bufferPosition, nalUnitLength); + if (nalUnitType == SPS_NAL_UNIT_TYPE && j == 0) { + codecs = + CodecSpecificDataUtil.buildHevcCodecStringFromSps( + new ParsableNalUnitBitArray(buffer, bufferPosition, nalUnitLength)); + } bufferPosition += nalUnitLength; data.skipBytes(nalUnitLength); } } + @Nullable List initializationData = csdLength == 0 ? null : Collections.singletonList(buffer); - return new HevcConfig(initializationData, lengthSizeMinusOne + 1); + return new HevcConfig(initializationData, lengthSizeMinusOne + 1, codecs); } catch (ArrayIndexOutOfBoundsException e) { throw new ParserException("Error parsing HEVC config", e); } } - private HevcConfig(@Nullable List initializationData, int nalUnitLengthFieldLength) { + private static final int SPS_NAL_UNIT_TYPE = 33; + + /** + * List of buffers containing the codec-specific data to be provided to the decoder, or {@code + * null} if not known. + * + * @see com.google.android.exoplayer2.Format#initializationData + */ + @Nullable public final List initializationData; + /** The length of the NAL unit length field in the bitstream's container, in bytes. */ + public final int nalUnitLengthFieldLength; + /** + * An RFC 6381 codecs string representing the video format, or {@code null} if not known. + * + * @see com.google.android.exoplayer2.Format#codecs + */ + @Nullable public final String codecs; + + private HevcConfig( + @Nullable List initializationData, + int nalUnitLengthFieldLength, + @Nullable String codecs) { this.initializationData = initializationData; this.nalUnitLengthFieldLength = nalUnitLengthFieldLength; + this.codecs = codecs; } } diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractor.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractor.java index 568385ad3b..53a6fbabea 100644 --- a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractor.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractor.java @@ -2093,6 +2093,7 @@ public class MatroskaExtractor implements Extractor { HevcConfig hevcConfig = HevcConfig.parse(new ParsableByteArray(getCodecPrivate(codecId))); initializationData = hevcConfig.initializationData; nalUnitLengthFieldLength = hevcConfig.nalUnitLengthFieldLength; + codecs = hevcConfig.codecs; break; case CODEC_ID_FOURCC: Pair> pair = diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java index 9d285fe8dc..ff63798583 100644 --- a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java @@ -1066,6 +1066,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; HevcConfig hevcConfig = HevcConfig.parse(parent); initializationData = hevcConfig.initializationData; out.nalUnitLengthFieldLength = hevcConfig.nalUnitLengthFieldLength; + codecs = hevcConfig.codecs; } else if (childAtomType == Atom.TYPE_dvcC || childAtomType == Atom.TYPE_dvvC) { @Nullable DolbyVisionConfig dolbyVisionConfig = DolbyVisionConfig.parse(parent); if (dolbyVisionConfig != null) { diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ts/H265Reader.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ts/H265Reader.java index ea23e1ef7a..0ef719c961 100644 --- a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ts/H265Reader.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ts/H265Reader.java @@ -24,6 +24,7 @@ import com.google.android.exoplayer2.extractor.ExtractorOutput; import com.google.android.exoplayer2.extractor.TrackOutput; import com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator; import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.util.CodecSpecificDataUtil; import com.google.android.exoplayer2.util.Log; import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.NalUnitUtil; @@ -336,9 +337,15 @@ public final class H265Reader implements ElementaryStreamReader { } } + // Parse the SPS to derive an RFC 6381 codecs string. + bitArray.reset(sps.nalData, 0, sps.nalLength); + bitArray.skipBits(24); // Skip start code. + String codecs = CodecSpecificDataUtil.buildHevcCodecStringFromSps(bitArray); + return new Format.Builder() .setId(formatId) .setSampleMimeType(MimeTypes.VIDEO_H265) + .setCodecs(codecs) .setWidth(picWidthInLumaSamples) .setHeight(picHeightInLumaSamples) .setPixelWidthHeightRatio(pixelWidthHeightRatio) diff --git a/testdata/src/test/assets/extractordumps/ts/sample_h265.ts.0.dump b/testdata/src/test/assets/extractordumps/ts/sample_h265.ts.0.dump index 1bf1d8af7f..ca03201f88 100644 --- a/testdata/src/test/assets/extractordumps/ts/sample_h265.ts.0.dump +++ b/testdata/src/test/assets/extractordumps/ts/sample_h265.ts.0.dump @@ -12,6 +12,7 @@ track 256: format 0: id = 1/256 sampleMimeType = video/hevc + codecs = hvc1.1.6.L90.90 width = 854 height = 480 initializationData: diff --git a/testdata/src/test/assets/extractordumps/ts/sample_h265.ts.1.dump b/testdata/src/test/assets/extractordumps/ts/sample_h265.ts.1.dump index 18dc2ebabe..7f26d0b1e9 100644 --- a/testdata/src/test/assets/extractordumps/ts/sample_h265.ts.1.dump +++ b/testdata/src/test/assets/extractordumps/ts/sample_h265.ts.1.dump @@ -12,6 +12,7 @@ track 256: format 0: id = 1/256 sampleMimeType = video/hevc + codecs = hvc1.1.6.L90.90 width = 854 height = 480 initializationData: diff --git a/testdata/src/test/assets/extractordumps/ts/sample_h265.ts.2.dump b/testdata/src/test/assets/extractordumps/ts/sample_h265.ts.2.dump index 1b81fef517..d22d002651 100644 --- a/testdata/src/test/assets/extractordumps/ts/sample_h265.ts.2.dump +++ b/testdata/src/test/assets/extractordumps/ts/sample_h265.ts.2.dump @@ -12,6 +12,7 @@ track 256: format 0: id = 1/256 sampleMimeType = video/hevc + codecs = hvc1.1.6.L90.90 width = 854 height = 480 initializationData: diff --git a/testdata/src/test/assets/extractordumps/ts/sample_h265.ts.3.dump b/testdata/src/test/assets/extractordumps/ts/sample_h265.ts.3.dump index 1dd24c870e..68a7e61768 100644 --- a/testdata/src/test/assets/extractordumps/ts/sample_h265.ts.3.dump +++ b/testdata/src/test/assets/extractordumps/ts/sample_h265.ts.3.dump @@ -12,6 +12,7 @@ track 256: format 0: id = 1/256 sampleMimeType = video/hevc + codecs = hvc1.1.6.L90.90 width = 854 height = 480 initializationData: diff --git a/testdata/src/test/assets/extractordumps/ts/sample_h265.ts.unknown_length.dump b/testdata/src/test/assets/extractordumps/ts/sample_h265.ts.unknown_length.dump index 09b1103840..920032dd23 100644 --- a/testdata/src/test/assets/extractordumps/ts/sample_h265.ts.unknown_length.dump +++ b/testdata/src/test/assets/extractordumps/ts/sample_h265.ts.unknown_length.dump @@ -9,6 +9,7 @@ track 256: format 0: id = 1/256 sampleMimeType = video/hevc + codecs = hvc1.1.6.L90.90 width = 854 height = 480 initializationData: