Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions RELEASENOTES.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,13 @@
custom format ordering and ABR selection priority beyond bitrate-only
ordering.
* Extractors:
* MPEG-TS: Add Dolby Vision support in HLS/TS streams. The extractor now
detects the `DOVI` registration descriptor and the `0xB0` Dolby Vision
video descriptor in the PMT, and routes HEVC-based Dolby Vision
elementary streams to the Dolby Vision decoder with the correct profile,
level, and colour info. This fixes a green/magenta tint on
non-cross-compatible profiles (e.g. profile 5) that was caused by the
HEVC base layer being decoded without DV signalling.
* MP4, MP3, and FLAC: Add `FLAG_DISABLE_ARTWORK_METADATA` to allow
discarding attached pictures and cover art metadata during container
parsing to reduce runtime memory consumption
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,15 @@ public final class NalUnitUtil {
/** H.265 suffixed supplemental enhancement information (SUFFIX_SEI_NUT). */
public static final int H265_NAL_UNIT_TYPE_SUFFIX_SEI = 40;

/**
* H.265 unspecified NAL unit type carrying a Dolby Vision RPU (enhancement metadata) in
* profile 5/7/8 streams.
*/
public static final int H265_NAL_UNIT_TYPE_DV_RPU = 62;

/** H.265 unspecified NAL unit type carrying a Dolby Vision enhancement layer. */
public static final int H265_NAL_UNIT_TYPE_DV_EL = 63;

/** H.265 unspecified NAL unit. */
public static final int H265_NAL_UNIT_TYPE_UNSPECIFIED = 48;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,9 @@ public TsPayloadReader createPayloadReader(int streamType, EsInfo esInfo) {
isSet(FLAG_DETECT_ACCESS_UNITS),
MimeTypes.VIDEO_MP2T));
case TsExtractor.TS_STREAM_TYPE_H265:
return new PesReader(new H265Reader(buildSeiReader(esInfo), MimeTypes.VIDEO_MP2T));
return new PesReader(
new H265Reader(
buildSeiReader(esInfo), MimeTypes.VIDEO_MP2T, esInfo.dolbyVisionConfig));
case TsExtractor.TS_STREAM_TYPE_SPLICE_INFO:
return isSet(FLAG_IGNORE_SPLICE_INFO_STREAM)
? null
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
import androidx.media3.common.util.ParsableByteArray;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.common.util.Util;
import androidx.media3.container.DolbyVisionConfig;
import androidx.media3.container.NalUnitUtil;
import androidx.media3.extractor.ExtractorOutput;
import androidx.media3.extractor.TrackOutput;
Expand All @@ -42,6 +43,7 @@ public final class H265Reader implements ElementaryStreamReader {

private final SeiReader seiReader;
private final String containerMimeType;
@Nullable private final DolbyVisionConfig dolbyVisionConfig;

private @MonotonicNonNull String formatId;
private @MonotonicNonNull TrackOutput output;
Expand Down Expand Up @@ -70,8 +72,22 @@ public final class H265Reader implements ElementaryStreamReader {
* @param containerMimeType The MIME type of the container holding the stream.
*/
public H265Reader(SeiReader seiReader, String containerMimeType) {
this(seiReader, containerMimeType, /* dolbyVisionConfig= */ null);
}

/**
* @param seiReader An SEI reader for consuming closed caption channels.
* @param containerMimeType The MIME type of the container holding the stream.
* @param dolbyVisionConfig The Dolby Vision configuration signalled in the container, or {@code
* null} if the stream is plain HEVC.
*/
public H265Reader(
SeiReader seiReader,
String containerMimeType,
@Nullable DolbyVisionConfig dolbyVisionConfig) {
this.seiReader = seiReader;
this.containerMimeType = containerMimeType;
this.dolbyVisionConfig = dolbyVisionConfig;
prefixFlags = new boolean[3];
vps = new NalUnitTargetBuffer(NalUnitUtil.H265_NAL_UNIT_TYPE_VPS, 128);
sps = new NalUnitTargetBuffer(NalUnitUtil.H265_NAL_UNIT_TYPE_SPS, 128);
Expand Down Expand Up @@ -214,7 +230,7 @@ private void endNalUnit(long position, int offset, int discardPadding, long pesT
sps.endNalUnit(discardPadding);
pps.endNalUnit(discardPadding);
if (vps.isCompleted() && sps.isCompleted() && pps.isCompleted()) {
Format format = parseMediaFormat(formatId, vps, sps, pps, containerMimeType);
Format format = parseMediaFormat(formatId, vps, sps, pps, containerMimeType, dolbyVisionConfig);
output.format(format);
checkState(format.maxNumReorderSamples != Format.NO_VALUE);
seiReader.setReorderingQueueSize(format.maxNumReorderSamples);
Expand Down Expand Up @@ -244,7 +260,8 @@ private static Format parseMediaFormat(
NalUnitTargetBuffer vps,
NalUnitTargetBuffer sps,
NalUnitTargetBuffer pps,
String containerMimeType) {
String containerMimeType,
@Nullable DolbyVisionConfig dolbyVisionConfig) {
// Build codec-specific data.
byte[] csdData = new byte[vps.nalLength + sps.nalLength + pps.nalLength];
System.arraycopy(vps.nalData, 0, csdData, 0, vps.nalLength);
Expand All @@ -267,20 +284,50 @@ private static Format parseMediaFormat(
spsData.profileTierLevel.constraintBytes,
spsData.profileTierLevel.generalLevelIdc);
}

String sampleMimeType = MimeTypes.VIDEO_H265;
@C.ColorSpace int colorSpace = spsData.colorSpace;
@C.ColorRange int colorRange = spsData.colorRange;
@C.ColorTransfer int colorTransfer = spsData.colorTransfer;
if (dolbyVisionConfig != null) {
// The PMT signals Dolby Vision: expose the track as Dolby Vision so the DV decoder is
// selected and configured with the correct profile, instead of decoding the HEVC base layer
// directly (which produces a green tint for non-backward-compatible profiles such as 5).
sampleMimeType = MimeTypes.VIDEO_DOLBY_VISION;
codecs = dolbyVisionConfig.codecs;
// Profile 5 is the only non-cross-compatible profile: its base layer uses the proprietary
// IPT-PQ-c2 colour space and the SPS VUI omits colour signalling, so the decoder receives
// unset colour aspects (0:0:0:0) and renders a green/magenta tint. Synthesize the colour info
// the DV pipeline expects. Cross-compatible profiles (8.x etc.) carry a standard HEVC base
// layer whose SPS already signals the correct colour (which may be HLG, not PQ), so leave
// those untouched.
if (dolbyVisionConfig.profile == 5) {
if (colorSpace == Format.NO_VALUE) {
colorSpace = C.COLOR_SPACE_BT2020;
}
if (colorTransfer == Format.NO_VALUE) {
colorTransfer = C.COLOR_TRANSFER_ST2084;
}
if (colorRange == Format.NO_VALUE) {
colorRange = C.COLOR_RANGE_LIMITED;
}
}
}

return new Format.Builder()
.setId(formatId)
.setContainerMimeType(containerMimeType)
.setSampleMimeType(MimeTypes.VIDEO_H265)
.setSampleMimeType(sampleMimeType)
.setCodecs(codecs)
.setWidth(spsData.width)
.setHeight(spsData.height)
.setDecodedWidth(spsData.decodedWidth)
.setDecodedHeight(spsData.decodedHeight)
.setColorInfo(
new ColorInfo.Builder()
.setColorSpace(spsData.colorSpace)
.setColorRange(spsData.colorRange)
.setColorTransfer(spsData.colorTransfer)
.setColorSpace(colorSpace)
.setColorRange(colorRange)
.setColorTransfer(colorTransfer)
.setLumaBitdepth(spsData.bitDepthLumaMinus8 + 8)
.setChromaBitdepth(spsData.bitDepthChromaMinus8 + 8)
.build())
Expand Down Expand Up @@ -414,7 +461,9 @@ private static boolean isPrefixNalUnit(int nalUnitType) {
/** Returns whether a NAL unit type is one that occurs in the VLC body of a sample. */
private static boolean isVclBodyNalUnit(int nalUnitType) {
return nalUnitType < NalUnitUtil.H265_NAL_UNIT_TYPE_VPS
|| nalUnitType == NalUnitUtil.H265_NAL_UNIT_TYPE_SUFFIX_SEI;
|| nalUnitType == NalUnitUtil.H265_NAL_UNIT_TYPE_SUFFIX_SEI
|| nalUnitType == NalUnitUtil.H265_NAL_UNIT_TYPE_DV_RPU
|| nalUnitType == NalUnitUtil.H265_NAL_UNIT_TYPE_DV_EL;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
import androidx.media3.common.util.TimestampAdjuster;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.common.util.Util;
import androidx.media3.container.DolbyVisionConfig;
import androidx.media3.extractor.Extractor;
import androidx.media3.extractor.ExtractorInput;
import androidx.media3.extractor.ExtractorOutput;
Expand Down Expand Up @@ -156,6 +157,7 @@ public static ExtractorsFactory newFactory(SubtitleParser.Factory subtitleParser
private static final long E_AC3_FORMAT_IDENTIFIER = 0x45414333;
private static final long AC4_FORMAT_IDENTIFIER = 0x41432d34;
private static final long HEVC_FORMAT_IDENTIFIER = 0x48455643;
private static final long DOVI_FORMAT_IDENTIFIER = 0x444f5649; // "DOVI"

private static final int BUFFER_SIZE = TS_PACKET_SIZE * 50;
private static final int SNIFF_TS_PACKET_COUNT = 5;
Expand Down Expand Up @@ -690,6 +692,7 @@ private class PmtReader implements SectionPayloadReader {
private static final int TS_PMT_DESC_DTS = 0x7B;
private static final int TS_PMT_DESC_DVB_EXT = 0x7F;
private static final int TS_PMT_DESC_DVBSUBS = 0x59;
private static final int TS_PMT_DESC_DOVI = 0xB0;

private static final int TS_PMT_DESC_DVB_EXT_AC4 = 0x15;
private static final int TS_PMT_DESC_DVB_EXT_DTS_HD = 0x0E;
Expand Down Expand Up @@ -856,6 +859,7 @@ private EsInfo readEsInfo(ParsableByteArray data, int length) {
String language = null;
@EsInfo.AudioType int audioType = AUDIO_TYPE_UNDEFINED;
List<DvbSubtitleInfo> dvbSubtitleInfos = null;
@Nullable DolbyVisionConfig dolbyVisionConfig = null;
while (data.getPosition() < descriptorsEndPosition) {
int descriptorTag = data.readUnsignedByte();
int descriptorLength = data.readUnsignedByte();
Expand All @@ -874,7 +878,17 @@ private EsInfo readEsInfo(ParsableByteArray data, int length) {
streamType = TS_STREAM_TYPE_AC4;
} else if (formatIdentifier == HEVC_FORMAT_IDENTIFIER) {
streamType = TS_STREAM_TYPE_H265;
} else if (formatIdentifier == DOVI_FORMAT_IDENTIFIER) {
// The DOVI registration descriptor signals that this is a Dolby Vision stream, but does
// not carry profile/level info. Full DV treatment (format and colour-info override)
// requires a co-located TS_PMT_DESC_DOVI (0xB0) descriptor in the same ES loop.
streamType = TS_STREAM_TYPE_H265;
}
} else if (descriptorTag == TS_PMT_DESC_DOVI) {
// Dolby Vision video descriptor: 16-bit dv_descriptor header layout matching the dvcC
// record (dv_version_major, dv_version_minor, then dv_profile/dv_level bits).
streamType = TS_STREAM_TYPE_H265;
dolbyVisionConfig = DolbyVisionConfig.parse(data);
} else if (descriptorTag == TS_PMT_DESC_AC3) { // AC-3_descriptor in DVB (ETSI EN 300 468)
streamType = TS_STREAM_TYPE_AC3;
} else if (descriptorTag == TS_PMT_DESC_EAC3) { // enhanced_AC-3_descriptor
Expand Down Expand Up @@ -920,7 +934,8 @@ private EsInfo readEsInfo(ParsableByteArray data, int length) {
language,
audioType,
dvbSubtitleInfos,
Arrays.copyOfRange(data.getData(), descriptorsStartPosition, descriptorsEndPosition));
Arrays.copyOfRange(data.getData(), descriptorsStartPosition, descriptorsEndPosition),
dolbyVisionConfig);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
import androidx.media3.common.util.ParsableByteArray;
import androidx.media3.common.util.TimestampAdjuster;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.container.DolbyVisionConfig;
import androidx.media3.extractor.ExtractorOutput;
import androidx.media3.extractor.TrackOutput;
import java.lang.annotation.Documented;
Expand Down Expand Up @@ -111,6 +112,8 @@ final class EsInfo {
public final @AudioType int audioType;
public final List<DvbSubtitleInfo> dvbSubtitleInfos;
public final byte[] descriptorBytes;
/** The Dolby Vision configuration signalled in the PMT, or {@code null} if not present. */
@Nullable public final DolbyVisionConfig dolbyVisionConfig;

/**
* @param streamType The type of the stream as defined by the {@link TsExtractor}{@code
Expand All @@ -119,13 +122,34 @@ final class EsInfo {
* @param audioType The audio type of the stream, as defined by ISO/IEC 13818-1, section 2.6.18.
* @param dvbSubtitleInfos Information about DVB subtitles associated to the stream.
* @param descriptorBytes The descriptor bytes associated to the stream.
* @deprecated Use {@link #EsInfo(int, String, int, List, byte[], DolbyVisionConfig)} instead.
*/
@Deprecated
public EsInfo(
int streamType,
@Nullable String language,
@AudioType int audioType,
@Nullable List<DvbSubtitleInfo> dvbSubtitleInfos,
byte[] descriptorBytes) {
this(streamType, language, audioType, dvbSubtitleInfos, descriptorBytes, null);
}

/**
* @param streamType The type of the stream as defined by the {@link TsExtractor}{@code
* .TS_STREAM_TYPE_*}.
* @param language The language of the stream, as defined by ISO/IEC 13818-1, section 2.6.18.
* @param audioType The audio type of the stream, as defined by ISO/IEC 13818-1, section 2.6.18.
* @param dvbSubtitleInfos Information about DVB subtitles associated to the stream.
* @param descriptorBytes The descriptor bytes associated to the stream.
* @param dolbyVisionConfig The Dolby Vision configuration signalled in the PMT, or {@code null}.
*/
public EsInfo(
int streamType,
@Nullable String language,
@AudioType int audioType,
@Nullable List<DvbSubtitleInfo> dvbSubtitleInfos,
byte[] descriptorBytes,
@Nullable DolbyVisionConfig dolbyVisionConfig) {
this.streamType = streamType;
this.language = language;
this.audioType = audioType;
Expand All @@ -134,6 +158,7 @@ public EsInfo(
? Collections.emptyList()
: Collections.unmodifiableList(dvbSubtitleInfos);
this.descriptorBytes = descriptorBytes;
this.dolbyVisionConfig = dolbyVisionConfig;
}
}

Expand Down