From 9b6caf9edaa873d2b2b77a8b449e36325273124c Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Fri, 8 Feb 2019 16:45:00 +0000 Subject: [PATCH] Merge #5438: Dolby Vision Imported from GitHub PR https://github.com/google/ExoPlayer/pull/5438 Plus the following changes: - Only support profile 5 (handling other profiles requires backward-compatibility changes in the renderer which are left for a later change.) - Only add KEY_PROFILE to the codec configuration MediaFormat for Dolby Vision. - In MediaCodecUtil support all DV profiles that Android has constants for. This includes ones that are "not supported for new applications". Since we don't extract these profiles, this is currently only for the benefit of custom extractors. - Misc code style fixes and reordering for consistency. Merge 37878b975c2bc082b0568e21cbe62bfcef97c10d into 67be9e77834c0dc9d2b5d462c01c2f39718c8817 PiperOrigin-RevId: 233066799 --- RELEASENOTES.md | 1 + .../exoplayer2/extractor/mp4/Atom.java | 10 ++- .../exoplayer2/extractor/mp4/AtomParsers.java | 47 +++++++++++--- .../exoplayer2/mediacodec/MediaCodecUtil.java | 59 +++++++++++++++++ .../android/exoplayer2/util/MimeTypes.java | 6 ++ .../exoplayer2/video/DolbyVisionConfig.java | 64 +++++++++++++++++++ .../video/MediaCodecVideoRenderer.java | 11 ++++ 7 files changed, 188 insertions(+), 10 deletions(-) create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/video/DolbyVisionConfig.java diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 90eddb7c08..16b42dcf9f 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -14,6 +14,7 @@ * Update `TrackSelection.Factory` interface to support creating all track selections together. * Do not retry failed loads whose error is `FileNotFoundException`. +* Support Dolby Vision extraction in MP4 and fMP4. * Offline: * Speed up removal of segmented downloads ([#5136](https://github.com/google/ExoPlayer/issues/5136)). diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/Atom.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/Atom.java index 8d78337617..d5fcf46025 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/Atom.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/Atom.java @@ -53,8 +53,16 @@ import java.util.List; public static final int TYPE_ftyp = Util.getIntegerCodeForString("ftyp"); public static final int TYPE_avc1 = Util.getIntegerCodeForString("avc1"); public static final int TYPE_avc3 = Util.getIntegerCodeForString("avc3"); + public static final int TYPE_avcC = Util.getIntegerCodeForString("avcC"); public static final int TYPE_hvc1 = Util.getIntegerCodeForString("hvc1"); public static final int TYPE_hev1 = Util.getIntegerCodeForString("hev1"); + public static final int TYPE_hvcC = Util.getIntegerCodeForString("hvcC"); + public static final int TYPE_dvav = Util.getIntegerCodeForString("dvav"); + public static final int TYPE_dva1 = Util.getIntegerCodeForString("dva1"); + public static final int TYPE_dvhe = Util.getIntegerCodeForString("dvhe"); + public static final int TYPE_dvh1 = Util.getIntegerCodeForString("dvh1"); + public static final int TYPE_dvcC = Util.getIntegerCodeForString("dvcC"); + public static final int TYPE_dvvC = Util.getIntegerCodeForString("dvvC"); public static final int TYPE_s263 = Util.getIntegerCodeForString("s263"); public static final int TYPE_d263 = Util.getIntegerCodeForString("d263"); public static final int TYPE_mdat = Util.getIntegerCodeForString("mdat"); @@ -83,8 +91,6 @@ import java.util.List; public static final int TYPE_mdia = Util.getIntegerCodeForString("mdia"); public static final int TYPE_minf = Util.getIntegerCodeForString("minf"); public static final int TYPE_stbl = Util.getIntegerCodeForString("stbl"); - public static final int TYPE_avcC = Util.getIntegerCodeForString("avcC"); - public static final int TYPE_hvcC = Util.getIntegerCodeForString("hvcC"); public static final int TYPE_esds = Util.getIntegerCodeForString("esds"); public static final int TYPE_moof = Util.getIntegerCodeForString("moof"); public static final int TYPE_traf = Util.getIntegerCodeForString("traf"); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java index 008a155d1f..3b4a5c0c5b 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java @@ -33,6 +33,7 @@ import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.ParsableByteArray; import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.video.AvcConfig; +import com.google.android.exoplayer2.video.DolbyVisionConfig; import com.google.android.exoplayer2.video.HevcConfig; import java.util.ArrayList; import java.util.Arrays; @@ -735,11 +736,19 @@ import java.util.List; int childAtomSize = stsd.readInt(); Assertions.checkArgument(childAtomSize > 0, "childAtomSize should be positive"); int childAtomType = stsd.readInt(); - if (childAtomType == Atom.TYPE_avc1 || childAtomType == Atom.TYPE_avc3 - || childAtomType == Atom.TYPE_encv || childAtomType == Atom.TYPE_mp4v - || childAtomType == Atom.TYPE_hvc1 || childAtomType == Atom.TYPE_hev1 - || childAtomType == Atom.TYPE_s263 || childAtomType == Atom.TYPE_vp08 - || childAtomType == Atom.TYPE_vp09) { + if (childAtomType == Atom.TYPE_avc1 + || childAtomType == Atom.TYPE_avc3 + || childAtomType == Atom.TYPE_encv + || childAtomType == Atom.TYPE_mp4v + || childAtomType == Atom.TYPE_hvc1 + || childAtomType == Atom.TYPE_hev1 + || childAtomType == Atom.TYPE_s263 + || childAtomType == Atom.TYPE_vp08 + || childAtomType == Atom.TYPE_vp09 + || childAtomType == Atom.TYPE_dvav + || childAtomType == Atom.TYPE_dva1 + || childAtomType == Atom.TYPE_dvhe + || childAtomType == Atom.TYPE_dvh1) { parseVideoSampleEntry(stsd, childAtomType, childStartPosition, childAtomSize, trackId, rotationDegrees, drmInitData, out, i); } else if (childAtomType == Atom.TYPE_mp4a @@ -852,6 +861,7 @@ import java.util.List; List initializationData = null; String mimeType = null; + String codecs = null; byte[] projectionData = null; @C.StereoMode int stereoMode = Format.NO_VALUE; @@ -882,6 +892,13 @@ import java.util.List; HevcConfig hevcConfig = HevcConfig.parse(parent); initializationData = hevcConfig.initializationData; out.nalUnitLengthFieldLength = hevcConfig.nalUnitLengthFieldLength; + } else if (childAtomType == Atom.TYPE_dvcC || childAtomType == Atom.TYPE_dvvC) { + DolbyVisionConfig dolbyVisionConfig = DolbyVisionConfig.parse(parent); + // TODO: Support profiles 4, 8 and 9 once we have a way to fall back to AVC/HEVC decoding. + if (dolbyVisionConfig != null && dolbyVisionConfig.profile == 5) { + codecs = dolbyVisionConfig.codecs; + mimeType = MimeTypes.VIDEO_DOLBY_VISION; + } } else if (childAtomType == Atom.TYPE_vpcC) { Assertions.checkState(mimeType == null); mimeType = (atomType == Atom.TYPE_vp08) ? MimeTypes.VIDEO_VP8 : MimeTypes.VIDEO_VP9; @@ -930,9 +947,23 @@ import java.util.List; return; } - out.format = Format.createVideoSampleFormat(Integer.toString(trackId), mimeType, null, - Format.NO_VALUE, Format.NO_VALUE, width, height, Format.NO_VALUE, initializationData, - rotationDegrees, pixelWidthHeightRatio, projectionData, stereoMode, null, drmInitData); + out.format = + Format.createVideoSampleFormat( + Integer.toString(trackId), + mimeType, + codecs, + /* bitrate= */ Format.NO_VALUE, + /* maxInputSize= */ Format.NO_VALUE, + width, + height, + /* frameRate= */ Format.NO_VALUE, + initializationData, + rotationDegrees, + pixelWidthHeightRatio, + projectionData, + stereoMode, + /* colorInfo= */ null, + drmInitData); } /** diff --git a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java index 95cf82ff6c..0530057973 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java @@ -71,6 +71,11 @@ public final class MediaCodecUtil { private static final Map HEVC_CODEC_STRING_TO_PROFILE_LEVEL; private static final String CODEC_ID_HEV1 = "hev1"; private static final String CODEC_ID_HVC1 = "hvc1"; + // Dolby Vision. + private static final Map DOLBY_VISION_STRING_TO_PROFILE; + private static final Map DOLBY_VISION_STRING_TO_LEVEL; + private static final String CODEC_ID_DVHE = "dvhe"; + private static final String CODEC_ID_DVH1 = "dvh1"; // MP4A AAC. private static final SparseIntArray MP4A_AUDIO_OBJECT_TYPE_TO_PROFILE; private static final String CODEC_ID_MP4A = "mp4a"; @@ -208,6 +213,9 @@ public final class MediaCodecUtil { case CODEC_ID_HEV1: case CODEC_ID_HVC1: return getHevcProfileAndLevel(codec, parts); + case CODEC_ID_DVHE: + case CODEC_ID_DVH1: + return getDolbyVisionProfileAndLevel(codec, parts); case CODEC_ID_AVC1: case CODEC_ID_AVC2: return getAvcProfileAndLevel(codec, parts); @@ -423,6 +431,34 @@ public final class MediaCodecUtil { && ("OMX.Exynos.AVC.Decoder".equals(name) || "OMX.Exynos.AVC.Decoder.secure".equals(name)); } + private static Pair getDolbyVisionProfileAndLevel( + String codec, String[] parts) { + if (parts.length < 3) { + // The codec has fewer parts than required by the Dolby Vision codec string format. + Log.w(TAG, "Ignoring malformed Dolby Vision codec string: " + codec); + return null; + } + // The profile_space gets ignored. + Matcher matcher = PROFILE_PATTERN.matcher(parts[1]); + if (!matcher.matches()) { + Log.w(TAG, "Ignoring malformed Dolby Vision codec string: " + codec); + return null; + } + String profileString = matcher.group(1); + Integer profile = DOLBY_VISION_STRING_TO_PROFILE.get(profileString); + if (profile == null) { + Log.w(TAG, "Unknown Dolby Vision profile string: " + profileString); + return null; + } + String levelString = parts[2]; + Integer level = DOLBY_VISION_STRING_TO_LEVEL.get(levelString); + if (level == null) { + Log.w(TAG, "Unknown Dolby Vision level string: " + levelString); + return null; + } + return new Pair<>(profile, level); + } + private static Pair getHevcProfileAndLevel(String codec, String[] parts) { if (parts.length < 4) { // The codec has fewer parts than required by the HEVC codec string format. @@ -783,6 +819,29 @@ public final class MediaCodecUtil { HEVC_CODEC_STRING_TO_PROFILE_LEVEL.put("H183", CodecProfileLevel.HEVCHighTierLevel61); HEVC_CODEC_STRING_TO_PROFILE_LEVEL.put("H186", CodecProfileLevel.HEVCHighTierLevel62); + DOLBY_VISION_STRING_TO_PROFILE = new HashMap<>(); + DOLBY_VISION_STRING_TO_PROFILE.put("00", CodecProfileLevel.DolbyVisionProfileDvavPer); + DOLBY_VISION_STRING_TO_PROFILE.put("01", CodecProfileLevel.DolbyVisionProfileDvavPen); + DOLBY_VISION_STRING_TO_PROFILE.put("02", CodecProfileLevel.DolbyVisionProfileDvheDer); + DOLBY_VISION_STRING_TO_PROFILE.put("03", CodecProfileLevel.DolbyVisionProfileDvheDen); + DOLBY_VISION_STRING_TO_PROFILE.put("04", CodecProfileLevel.DolbyVisionProfileDvheDtr); + DOLBY_VISION_STRING_TO_PROFILE.put("05", CodecProfileLevel.DolbyVisionProfileDvheStn); + DOLBY_VISION_STRING_TO_PROFILE.put("06", CodecProfileLevel.DolbyVisionProfileDvheDth); + DOLBY_VISION_STRING_TO_PROFILE.put("07", CodecProfileLevel.DolbyVisionProfileDvheDtb); + DOLBY_VISION_STRING_TO_PROFILE.put("08", CodecProfileLevel.DolbyVisionProfileDvheSt); + DOLBY_VISION_STRING_TO_PROFILE.put("09", CodecProfileLevel.DolbyVisionProfileDvavSe); + + DOLBY_VISION_STRING_TO_LEVEL = new HashMap<>(); + DOLBY_VISION_STRING_TO_LEVEL.put("01", CodecProfileLevel.DolbyVisionLevelHd24); + DOLBY_VISION_STRING_TO_LEVEL.put("02", CodecProfileLevel.DolbyVisionLevelHd30); + DOLBY_VISION_STRING_TO_LEVEL.put("03", CodecProfileLevel.DolbyVisionLevelFhd24); + DOLBY_VISION_STRING_TO_LEVEL.put("04", CodecProfileLevel.DolbyVisionLevelFhd30); + DOLBY_VISION_STRING_TO_LEVEL.put("05", CodecProfileLevel.DolbyVisionLevelFhd60); + DOLBY_VISION_STRING_TO_LEVEL.put("06", CodecProfileLevel.DolbyVisionLevelUhd24); + DOLBY_VISION_STRING_TO_LEVEL.put("07", CodecProfileLevel.DolbyVisionLevelUhd30); + DOLBY_VISION_STRING_TO_LEVEL.put("08", CodecProfileLevel.DolbyVisionLevelUhd48); + DOLBY_VISION_STRING_TO_LEVEL.put("09", CodecProfileLevel.DolbyVisionLevelUhd60); + MP4A_AUDIO_OBJECT_TYPE_TO_PROFILE = new SparseIntArray(); MP4A_AUDIO_OBJECT_TYPE_TO_PROFILE.put(1, CodecProfileLevel.AACObjectMain); MP4A_AUDIO_OBJECT_TYPE_TO_PROFILE.put(2, CodecProfileLevel.AACObjectLC); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/MimeTypes.java b/library/core/src/main/java/com/google/android/exoplayer2/util/MimeTypes.java index c5ce93a239..003496a014 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/MimeTypes.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/MimeTypes.java @@ -41,6 +41,7 @@ public final class MimeTypes { public static final String VIDEO_MPEG = BASE_TYPE_VIDEO + "/mpeg"; public static final String VIDEO_MPEG2 = BASE_TYPE_VIDEO + "/mpeg2"; public static final String VIDEO_VC1 = BASE_TYPE_VIDEO + "/wvc1"; + public static final String VIDEO_DOLBY_VISION = BASE_TYPE_VIDEO + "/dolby-vision"; public static final String VIDEO_UNKNOWN = BASE_TYPE_VIDEO + "/x-unknown"; public static final String AUDIO_MP4 = BASE_TYPE_AUDIO + "/mp4"; @@ -213,6 +214,11 @@ public final class MimeTypes { return MimeTypes.VIDEO_H264; } else if (codec.startsWith("hev1") || codec.startsWith("hvc1")) { return MimeTypes.VIDEO_H265; + } else if (codec.startsWith("dvav") + || codec.startsWith("dva1") + || codec.startsWith("dvhe") + || codec.startsWith("dvh1")) { + return MimeTypes.VIDEO_DOLBY_VISION; } else if (codec.startsWith("vp9") || codec.startsWith("vp09")) { return MimeTypes.VIDEO_VP9; } else if (codec.startsWith("vp8") || codec.startsWith("vp08")) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/video/DolbyVisionConfig.java b/library/core/src/main/java/com/google/android/exoplayer2/video/DolbyVisionConfig.java new file mode 100644 index 0000000000..3a7c12dd14 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/video/DolbyVisionConfig.java @@ -0,0 +1,64 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.video; + +import android.support.annotation.Nullable; +import com.google.android.exoplayer2.util.ParsableByteArray; + +/** Dolby Vision configuration data. */ +public final class DolbyVisionConfig { + + /** + * Parses Dolby Vision configuration data. + * + * @param data A {@link ParsableByteArray}, whose position is set to the start of the Dolby Vision + * configuration data to parse. + * @return The {@link DolbyVisionConfig} corresponding to the configuration, or {@code null} if + * the configuration isn't supported. + */ + @Nullable + public static DolbyVisionConfig parse(ParsableByteArray data) { + data.skipBytes(2); // dv_version_major, dv_version_minor + int profileData = data.readUnsignedByte(); + int dvProfile = (profileData >> 1); + int dvLevel = ((profileData & 0x1) << 5) | ((data.readUnsignedByte() >> 3) & 0x1F); + String codecsPrefix; + if (dvProfile == 4 || dvProfile == 5) { + codecsPrefix = "dvhe"; + } else if (dvProfile == 8) { + codecsPrefix = "hev1"; + } else if (dvProfile == 9) { + codecsPrefix = "avc3"; + } else { + return null; + } + String codecs = codecsPrefix + ".0" + dvProfile + ".0" + dvLevel; + return new DolbyVisionConfig(dvProfile, dvLevel, codecs); + } + + /** The profile number. */ + public final int profile; + /** The level number. */ + public final int level; + /** The RFC 6381 codecs string. */ + public final String codecs; + + private DolbyVisionConfig(int profile, int level, String codecs) { + this.profile = profile; + this.level = level; + this.codecs = codecs; + } +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java index 68e98633d6..a4f67b1eda 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java @@ -28,6 +28,7 @@ import android.os.SystemClock; import android.support.annotation.CallSuper; import android.support.annotation.NonNull; import android.support.annotation.Nullable; +import android.util.Pair; import android.view.Surface; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlaybackException; @@ -1057,6 +1058,16 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { MediaFormatUtil.maybeSetFloat(mediaFormat, MediaFormat.KEY_FRAME_RATE, format.frameRate); MediaFormatUtil.maybeSetInteger(mediaFormat, MediaFormat.KEY_ROTATION, format.rotationDegrees); MediaFormatUtil.maybeSetColorInfo(mediaFormat, format.colorInfo); + if (MimeTypes.VIDEO_DOLBY_VISION.equals(format.sampleMimeType)) { + // Some phones require the profile to be set on the codec. + // See https://github.com/google/ExoPlayer/pull/5438. + Pair codecProfileAndLevel = + MediaCodecUtil.getCodecProfileAndLevel(format.codecs); + if (codecProfileAndLevel != null) { + MediaFormatUtil.maybeSetInteger( + mediaFormat, MediaFormat.KEY_PROFILE, codecProfileAndLevel.first); + } + } // Set codec max values. mediaFormat.setInteger(MediaFormat.KEY_MAX_WIDTH, codecMaxValues.width); mediaFormat.setInteger(MediaFormat.KEY_MAX_HEIGHT, codecMaxValues.height);