From e7a50e92e2fbde55ea2427e6bd50cc6d331c3d56 Mon Sep 17 00:00:00 2001 From: ybai001 Date: Thu, 9 Oct 2025 15:46:04 +0800 Subject: [PATCH] Add Dolby Vision Profile 10 Playback Support This PR adds Dolby Vision profile 10 (AV1 based) playback support. Profile 10 includes profile 10.0 (not backward compatible), profile 10.1 (HDR10 compatible) and profile 10.4 (HLG compatible). --- .../androidx/media3/common/MimeTypes.java | 7 +-- .../androidx/media3/container/Mp4Box.java | 3 ++ .../mediacodec/MediaCodecRenderer.java | 5 +- .../exoplayer/mediacodec/MediaCodecUtil.java | 33 +++++++++++++ .../dash/manifest/DashManifestParser.java | 46 ++++++++++++++++++ .../media3/exoplayer/hls/HlsMediaPeriod.java | 1 + .../hls/playlist/HlsPlaylistParser.java | 47 +++++++++++++++++++ .../media3/extractor/mp4/BoxParser.java | 3 +- 8 files changed, 140 insertions(+), 5 deletions(-) diff --git a/libraries/common/src/main/java/androidx/media3/common/MimeTypes.java b/libraries/common/src/main/java/androidx/media3/common/MimeTypes.java index d64755d6df..56ce77407f 100644 --- a/libraries/common/src/main/java/androidx/media3/common/MimeTypes.java +++ b/libraries/common/src/main/java/androidx/media3/common/MimeTypes.java @@ -444,7 +444,8 @@ public static String getMediaMimeType(@Nullable String codec) { } else if (codec.startsWith("dvav") || codec.startsWith("dva1") || codec.startsWith("dvhe") - || codec.startsWith("dvh1")) { + || codec.startsWith("dvh1") + || codec.startsWith("dav1")) { return MimeTypes.VIDEO_DOLBY_VISION; } else if (codec.startsWith("av01")) { return MimeTypes.VIDEO_AV1; @@ -599,8 +600,8 @@ public static boolean isDolbyVisionCodec( if (codecs == null) { return false; } - if (codecs.startsWith("dvhe") || codecs.startsWith("dvh1")) { - // profile 5 + if (codecs.startsWith("dvhe") || codecs.startsWith("dvh1") || codecs.startsWith("dav1")) { + // profiles 5, 10.0 and 20.0 return true; } if (supplementalCodecs == null) { diff --git a/libraries/container/src/main/java/androidx/media3/container/Mp4Box.java b/libraries/container/src/main/java/androidx/media3/container/Mp4Box.java index 937d7a470f..08e6d17d33 100644 --- a/libraries/container/src/main/java/androidx/media3/container/Mp4Box.java +++ b/libraries/container/src/main/java/androidx/media3/container/Mp4Box.java @@ -117,6 +117,9 @@ public abstract class Mp4Box { @SuppressWarnings("ConstantCaseForConstants") public static final int TYPE_dvvC = 0x64767643; + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_dav1 = 0x64617631; + @SuppressWarnings("ConstantCaseForConstants") public static final int TYPE_dvwC = 0x64767743; diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/mediacodec/MediaCodecRenderer.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/mediacodec/MediaCodecRenderer.java index 5d2dfc4f45..b76d73329f 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/mediacodec/MediaCodecRenderer.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/mediacodec/MediaCodecRenderer.java @@ -1682,7 +1682,10 @@ protected DecoderReuseEvaluation onInputFormatChanged(FormatHolder formatHolder) // * b/229399008#comment9 // * https://github.com/androidx/media/issues/2408 if ((Objects.equals(newFormat.sampleMimeType, MimeTypes.VIDEO_AV1) - || Objects.equals(newFormat.sampleMimeType, MimeTypes.VIDEO_VP9)) + || Objects.equals(newFormat.sampleMimeType, MimeTypes.VIDEO_VP9) + || (Objects.equals(newFormat.sampleMimeType, MimeTypes.VIDEO_DOLBY_VISION) + && Objects.equals(MediaCodecUtil.getDolbyVisionBlMimeType(newFormat), + MimeTypes.VIDEO_AV1))) && !newFormat.initializationData.isEmpty()) { newFormat = newFormat.buildUpon().setInitializationData(null).build(); } diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/mediacodec/MediaCodecUtil.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/mediacodec/MediaCodecUtil.java index 887bfbf63f..ed4d922076 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/mediacodec/MediaCodecUtil.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/mediacodec/MediaCodecUtil.java @@ -31,6 +31,7 @@ import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; import androidx.annotation.VisibleForTesting; +import androidx.media3.common.C; import androidx.media3.common.Format; import androidx.media3.common.MimeTypes; import androidx.media3.common.util.CodecSpecificDataUtil; @@ -365,6 +366,33 @@ public static Pair getHevcBaseLayerCodecProfileAndLevel(Format return getHevcProfileAndLevel(codecs, parts, format.colorInfo); } + /** + * Returns a Dolby Vision base layer codec MIME type of the provided {@link Format}. + * + * @param format The media format. + * @return A Dolby Vision base layer MIME type, or null if a Dolby Vision profile is not + * identified. + */ + @Nullable + public static String getDolbyVisionBlMimeType(Format format) { + + if (MimeTypes.VIDEO_DOLBY_VISION.equals(format.sampleMimeType)) { + @Nullable Pair codecProfileAndLevel = getCodecProfileAndLevel(format); + if (codecProfileAndLevel != null) { + int profile = codecProfileAndLevel.first; + if (profile == CodecProfileLevel.DolbyVisionProfileDvheDtr + || profile == CodecProfileLevel.DolbyVisionProfileDvheSt) { + return MimeTypes.VIDEO_H265; + } else if (profile == CodecProfileLevel.DolbyVisionProfileDvavSe) { + return MimeTypes.VIDEO_H264; + } else if (profile == CodecProfileLevel.DolbyVisionProfileDvav110) { + return MimeTypes.VIDEO_AV1; + } + } + } + return null; + } + /** * Returns an alternative codec MIME type (besides the default {@link Format#sampleMimeType}) that * can be used to decode samples of the provided {@link Format}. @@ -394,6 +422,11 @@ public static String getAlternativeCodecMimeType(Format format) { } else if (profile == CodecProfileLevel.DolbyVisionProfileDvavSe) { return MimeTypes.VIDEO_H264; } else if (profile == CodecProfileLevel.DolbyVisionProfileDvav110) { + if (format.colorInfo != null + && format.colorInfo.colorTransfer == C.COLOR_TRANSFER_ST2084 + && format.colorInfo.colorRange == C.COLOR_RANGE_FULL) { + return null; + } return MimeTypes.VIDEO_AV1; } } diff --git a/libraries/exoplayer_dash/src/main/java/androidx/media3/exoplayer/dash/manifest/DashManifestParser.java b/libraries/exoplayer_dash/src/main/java/androidx/media3/exoplayer/dash/manifest/DashManifestParser.java index abd50fee48..6b73e64411 100644 --- a/libraries/exoplayer_dash/src/main/java/androidx/media3/exoplayer/dash/manifest/DashManifestParser.java +++ b/libraries/exoplayer_dash/src/main/java/androidx/media3/exoplayer/dash/manifest/DashManifestParser.java @@ -27,6 +27,7 @@ import android.util.Xml; import androidx.annotation.Nullable; import androidx.media3.common.C; +import androidx.media3.common.ColorInfo; import androidx.media3.common.DrmInitData; import androidx.media3.common.DrmInitData.SchemeData; import androidx.media3.common.Format; @@ -848,6 +849,9 @@ protected Format buildFormat( codecs = MimeTypes.CODEC_E_AC3_JOC; } } + + ColorInfo colorInfo = getColorInfoForFormat(codecs, supplementalCodecs, supplementalProfiles); + if (MimeTypes.isDolbyVisionCodec(codecs, supplementalCodecs)) { sampleMimeType = MimeTypes.VIDEO_DOLBY_VISION; codecs = supplementalCodecs != null ? supplementalCodecs : codecs; @@ -868,6 +872,7 @@ protected Format buildFormat( .setPeakBitrate(bitrate) .setSelectionFlags(selectionFlags) .setRoleFlags(roleFlags) + .setColorInfo(colorInfo) .setLanguage(language) .setTileCountHorizontal(tileCounts != null ? tileCounts.first : Format.NO_VALUE) .setTileCountVertical(tileCounts != null ? tileCounts.second : Format.NO_VALUE); @@ -2174,6 +2179,47 @@ private boolean isDvbProfileDeclared(String[] profiles) { return false; } + private static ColorInfo getColorInfoForFormat( + @Nullable String codecs, + @Nullable String supplementalCodecs, + @Nullable String supplementalProfiles) { + + @C.ColorSpace int colorSpace = Format.NO_VALUE; + @C.ColorRange int colorRange = Format.NO_VALUE; + @C.ColorTransfer int colorTransfer = Format.NO_VALUE; + + if (MimeTypes.isDolbyVisionCodec(codecs, supplementalCodecs)) { + if (codecs.startsWith("dvhe") || codecs.startsWith("dvh1") || codecs.startsWith("dav1")) { + // profiles 5, 10.0 and 20.0 + colorSpace = C.COLOR_SPACE_BT2020; + colorTransfer = C.COLOR_TRANSFER_ST2084; + colorRange = C.COLOR_RANGE_FULL; + } else if (supplementalProfiles != null) { + if (supplementalProfiles.equals("db1p")) { + //BL signal cross-compatibility ID = 1 (e.g profile 8.1) + colorSpace = C.COLOR_SPACE_BT2020; + colorTransfer = C.COLOR_TRANSFER_ST2084; + colorRange = C.COLOR_RANGE_LIMITED; + } else if (supplementalProfiles.startsWith("db4")) { // db4g or db4h + //BL signal cross-compatibility ID = 4 (e.g profile 8.4) + colorSpace = C.COLOR_SPACE_BT2020; + colorTransfer = C.COLOR_TRANSFER_HLG; + colorRange = C.COLOR_RANGE_LIMITED; + } + } + } + + if (colorSpace == Format.NO_VALUE) { + return null; + } + + return new ColorInfo.Builder() + .setColorSpace(colorSpace) + .setColorRange(colorRange) + .setColorTransfer(colorTransfer) + .build(); + } + /** A parsed Representation element. */ protected static final class RepresentationInfo { diff --git a/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/HlsMediaPeriod.java b/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/HlsMediaPeriod.java index 40b38a3218..e7308320c0 100644 --- a/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/HlsMediaPeriod.java +++ b/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/HlsMediaPeriod.java @@ -913,6 +913,7 @@ private static Format deriveVideoFormat(Format variantFormat) { .setFrameRate(variantFormat.frameRate) .setSelectionFlags(variantFormat.selectionFlags) .setRoleFlags(variantFormat.roleFlags) + .setColorInfo(variantFormat.colorInfo) .build(); } diff --git a/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/playlist/HlsPlaylistParser.java b/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/playlist/HlsPlaylistParser.java index 9b21486832..45ac43fd67 100644 --- a/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/playlist/HlsPlaylistParser.java +++ b/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/playlist/HlsPlaylistParser.java @@ -29,6 +29,7 @@ import android.util.Base64; import androidx.annotation.Nullable; import androidx.media3.common.C; +import androidx.media3.common.ColorInfo; import androidx.media3.common.DrmInitData; import androidx.media3.common.DrmInitData.SchemeData; import androidx.media3.common.Format; @@ -394,6 +395,47 @@ private static int skipIgnorableWhitespace(BufferedReader reader, boolean skipLi return c; } + private static ColorInfo getColorInfoForFormat( + @Nullable String codecs, + @Nullable String supplementalCodecs, + @Nullable String supplementalProfiles) { + + @C.ColorSpace int colorSpace = Format.NO_VALUE; + @C.ColorRange int colorRange = Format.NO_VALUE; + @C.ColorTransfer int colorTransfer = Format.NO_VALUE; + + if (MimeTypes.isDolbyVisionCodec(codecs, supplementalCodecs)) { + if (codecs.startsWith("dvhe") || codecs.startsWith("dvh1") || codecs.startsWith("dav1")) { + // profiles 5, 10.0 and 20.0 + colorSpace = C.COLOR_SPACE_BT2020; + colorTransfer = C.COLOR_TRANSFER_ST2084; + colorRange = C.COLOR_RANGE_FULL; + } else if (supplementalProfiles != null) { + if (supplementalProfiles.equals("db1p")) { + //BL signal cross-compatibility ID = 1 (e.g profile 8.1) + colorSpace = C.COLOR_SPACE_BT2020; + colorTransfer = C.COLOR_TRANSFER_ST2084; + colorRange = C.COLOR_RANGE_LIMITED; + } else if (supplementalProfiles.startsWith("db4")) { // db4g or db4h + //BL signal cross-compatibility ID = 4 (e.g profile 8.4) + colorSpace = C.COLOR_SPACE_BT2020; + colorTransfer = C.COLOR_TRANSFER_HLG; + colorRange = C.COLOR_RANGE_LIMITED; + } + } + } + + if (colorSpace == Format.NO_VALUE) { + return null; + } + + return new ColorInfo.Builder() + .setColorSpace(colorSpace) + .setColorRange(colorRange) + .setColorTransfer(colorTransfer) + .build(); + } + private static boolean isDolbyVisionFormat( @Nullable String videoRange, @Nullable String codecs, @@ -484,6 +526,10 @@ private static HlsMultivariantPlaylist parseMultivariantPlaylist( supplementalProfiles = codecsAndProfiles[1]; } } + + ColorInfo colorInfo = + getColorInfoForFormat(codecs, supplementalCodecs, supplementalProfiles); + String videoCodecs = Util.getCodecsOfType(codecs, C.TRACK_TYPE_VIDEO); if (isDolbyVisionFormat( videoRange, videoCodecs, supplementalCodecs, supplementalProfiles)) { @@ -545,6 +591,7 @@ private static HlsMultivariantPlaylist parseMultivariantPlaylist( .setHeight(height) .setFrameRate(frameRate) .setRoleFlags(roleFlags) + .setColorInfo(colorInfo) .build(); Variant variant = new Variant( diff --git a/libraries/extractor/src/main/java/androidx/media3/extractor/mp4/BoxParser.java b/libraries/extractor/src/main/java/androidx/media3/extractor/mp4/BoxParser.java index b6eddbb391..fd99894e71 100644 --- a/libraries/extractor/src/main/java/androidx/media3/extractor/mp4/BoxParser.java +++ b/libraries/extractor/src/main/java/androidx/media3/extractor/mp4/BoxParser.java @@ -1228,7 +1228,8 @@ private static StsdData parseStsd( || childAtomType == Mp4Box.TYPE_dva1 || childAtomType == Mp4Box.TYPE_dvhe || childAtomType == Mp4Box.TYPE_dvh1 - || childAtomType == Mp4Box.TYPE_apv1) { + || childAtomType == Mp4Box.TYPE_apv1 + || childAtomType == Mp4Box.TYPE_dav1) { parseVideoSampleEntry( stsd, childAtomType,