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);