Skip to content

Commit

Permalink
Merge pull request #162 from ittiam-systems:rtp-mp4a-latm
Browse files Browse the repository at this point in the history
PiperOrigin-RevId: 482490230
  • Loading branch information
rohitjoins committed Oct 24, 2022
2 parents a413b47 + ce98d6d commit fd2ba37
Show file tree
Hide file tree
Showing 17 changed files with 477 additions and 58 deletions.
Expand Up @@ -37,22 +37,23 @@
*/
public final class RtpPayloadFormat {

private static final String RTP_MEDIA_AC3 = "AC3";
private static final String RTP_MEDIA_AMR = "AMR";
private static final String RTP_MEDIA_AMR_WB = "AMR-WB";
private static final String RTP_MEDIA_MPEG4_GENERIC = "MPEG4-GENERIC";
private static final String RTP_MEDIA_MPEG4_VIDEO = "MP4V-ES";
private static final String RTP_MEDIA_H263_1998 = "H263-1998";
private static final String RTP_MEDIA_H263_2000 = "H263-2000";
private static final String RTP_MEDIA_H264 = "H264";
private static final String RTP_MEDIA_H265 = "H265";
private static final String RTP_MEDIA_OPUS = "OPUS";
private static final String RTP_MEDIA_PCM_L8 = "L8";
private static final String RTP_MEDIA_PCM_L16 = "L16";
private static final String RTP_MEDIA_PCMA = "PCMA";
private static final String RTP_MEDIA_PCMU = "PCMU";
private static final String RTP_MEDIA_VP8 = "VP8";
private static final String RTP_MEDIA_VP9 = "VP9";
public static final String RTP_MEDIA_AC3 = "AC3";
public static final String RTP_MEDIA_AMR = "AMR";
public static final String RTP_MEDIA_AMR_WB = "AMR-WB";
public static final String RTP_MEDIA_MPEG4_GENERIC = "MPEG4-GENERIC";
public static final String RTP_MEDIA_MPEG4_LATM_AUDIO = "MP4A-LATM";
public static final String RTP_MEDIA_MPEG4_VIDEO = "MP4V-ES";
public static final String RTP_MEDIA_H263_1998 = "H263-1998";
public static final String RTP_MEDIA_H263_2000 = "H263-2000";
public static final String RTP_MEDIA_H264 = "H264";
public static final String RTP_MEDIA_H265 = "H265";
public static final String RTP_MEDIA_OPUS = "OPUS";
public static final String RTP_MEDIA_PCM_L8 = "L8";
public static final String RTP_MEDIA_PCM_L16 = "L16";
public static final String RTP_MEDIA_PCMA = "PCMA";
public static final String RTP_MEDIA_PCMU = "PCMU";
public static final String RTP_MEDIA_VP8 = "VP8";
public static final String RTP_MEDIA_VP9 = "VP9";

/** Returns whether the format of a {@link MediaDescription} is supported. */
public static boolean isFormatSupported(MediaDescription mediaDescription) {
Expand All @@ -64,8 +65,9 @@ public static boolean isFormatSupported(MediaDescription mediaDescription) {
case RTP_MEDIA_H263_2000:
case RTP_MEDIA_H264:
case RTP_MEDIA_H265:
case RTP_MEDIA_MPEG4_VIDEO:
case RTP_MEDIA_MPEG4_GENERIC:
case RTP_MEDIA_MPEG4_LATM_AUDIO:
case RTP_MEDIA_MPEG4_VIDEO:
case RTP_MEDIA_OPUS:
case RTP_MEDIA_PCM_L8:
case RTP_MEDIA_PCM_L16:
Expand Down Expand Up @@ -95,6 +97,7 @@ public static String getMimeTypeFromRtpMediaType(String mediaType) {
case RTP_MEDIA_AMR_WB:
return MimeTypes.AUDIO_AMR_WB;
case RTP_MEDIA_MPEG4_GENERIC:
case RTP_MEDIA_MPEG4_LATM_AUDIO:
return MimeTypes.AUDIO_AAC;
case RTP_MEDIA_OPUS:
return MimeTypes.AUDIO_OPUS;
Expand Down Expand Up @@ -140,6 +143,8 @@ public static String getMimeTypeFromRtpMediaType(String mediaType) {
public final Format format;
/** The format parameters, mapped from the SDP FMTP attribute (RFC2327 Page 22). */
public final ImmutableMap<String, String> fmtpParameters;
/** The RTP media encoding. */
public final String mediaEncoding;

/**
* Creates a new instance.
Expand All @@ -151,13 +156,19 @@ public static String getMimeTypeFromRtpMediaType(String mediaType) {
* @param fmtpParameters The format parameters, from the SDP FMTP attribute (RFC2327 Page 22),
* empty if unset. The keys and values are specified in the RFCs for specific formats. For
* instance, RFC3640 Section 4.1 defines keys like profile-level-id and config.
* @param mediaEncoding The RTP media encoding.
*/
public RtpPayloadFormat(
Format format, int rtpPayloadType, int clockRate, Map<String, String> fmtpParameters) {
Format format,
int rtpPayloadType,
int clockRate,
Map<String, String> fmtpParameters,
String mediaEncoding) {
this.rtpPayloadType = rtpPayloadType;
this.clockRate = clockRate;
this.format = format;
this.fmtpParameters = ImmutableMap.copyOf(fmtpParameters);
this.mediaEncoding = mediaEncoding;
}

@Override
Expand All @@ -172,7 +183,8 @@ public boolean equals(@Nullable Object o) {
return rtpPayloadType == that.rtpPayloadType
&& clockRate == that.clockRate
&& format.equals(that.format)
&& fmtpParameters.equals(that.fmtpParameters);
&& fmtpParameters.equals(that.fmtpParameters)
&& mediaEncoding.equals(that.mediaEncoding);
}

@Override
Expand All @@ -182,6 +194,7 @@ public int hashCode() {
result = 31 * result + clockRate;
result = 31 * result + format.hashCode();
result = 31 * result + fmtpParameters.hashCode();
result = 31 * result + mediaEncoding.hashCode();
return result;
}
}
Expand Up @@ -30,10 +30,12 @@
import androidx.annotation.VisibleForTesting;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.ParserException;
import com.google.android.exoplayer2.audio.AacUtil;
import com.google.android.exoplayer2.util.CodecSpecificDataUtil;
import com.google.android.exoplayer2.util.MimeTypes;
import com.google.android.exoplayer2.util.NalUnitUtil;
import com.google.android.exoplayer2.util.ParsableBitArray;
import com.google.android.exoplayer2.util.Util;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
Expand All @@ -50,7 +52,8 @@
private static final String PARAMETER_H265_SPROP_PPS = "sprop-pps";
private static final String PARAMETER_H265_SPROP_VPS = "sprop-vps";
private static final String PARAMETER_H265_SPROP_MAX_DON_DIFF = "sprop-max-don-diff";
private static final String PARAMETER_MP4V_CONFIG = "config";
private static final String PARAMETER_MP4A_CONFIG = "config";
private static final String PARAMETER_MP4A_C_PRESENT = "cpresent";

/** Prefix for the RFC6381 codecs string for AAC formats. */
private static final String AAC_CODECS_PREFIX = "mp4a.40.";
Expand Down Expand Up @@ -206,6 +209,23 @@ public int hashCode() {
case MimeTypes.AUDIO_AAC:
checkArgument(channelCount != C.INDEX_UNSET);
checkArgument(!fmtpParameters.isEmpty());
if (mediaEncoding.equals(RtpPayloadFormat.RTP_MEDIA_MPEG4_LATM_AUDIO)) {
// cpresent is defined in RFC3016 Section 5.3. cpresent=0 means the config fmtp parameter
// must exist.
checkArgument(
fmtpParameters.containsKey(PARAMETER_MP4A_C_PRESENT)
&& fmtpParameters.get(PARAMETER_MP4A_C_PRESENT).equals("0"),
"Only supports cpresent=0 in AAC audio.");
@Nullable String config = fmtpParameters.get(PARAMETER_MP4A_CONFIG);
checkNotNull(config, "AAC audio stream must include config fmtp parameter");
// config is a hex string.
checkArgument(config.length() % 2 == 0, "Malformat MPEG4 config: " + config);
AacUtil.Config aacConfig = parseAacStreamMuxConfig(config);
formatBuilder
.setSampleRate(aacConfig.sampleRateHz)
.setChannelCount(aacConfig.channelCount)
.setCodecs(aacConfig.codecs);
}
processAacFmtpAttribute(formatBuilder, fmtpParameters, channelCount, clockRate);
break;
case MimeTypes.AUDIO_AMR_NB:
Expand Down Expand Up @@ -265,7 +285,8 @@ public int hashCode() {
}

checkArgument(clockRate > 0);
return new RtpPayloadFormat(formatBuilder.build(), rtpPayloadType, clockRate, fmtpParameters);
return new RtpPayloadFormat(
formatBuilder.build(), rtpPayloadType, clockRate, fmtpParameters, mediaEncoding);
}

private static int inferChannelCount(int encodingParameter, String mimeType) {
Expand Down Expand Up @@ -298,9 +319,29 @@ private static void processAacFmtpAttribute(
AacUtil.buildAacLcAudioSpecificConfig(sampleRate, channelCount)));
}

/**
* Returns the {@link AacUtil.Config} by parsing the MPEG4 Audio Stream Mux configuration.
*
* <p>fmtp attribute {@code config} includes the MPEG4 Audio Stream Mux configuration
* (ISO/IEC14496-3, Chapter 1.7.3).
*/
private static AacUtil.Config parseAacStreamMuxConfig(String streamMuxConfig) {
ParsableBitArray config = new ParsableBitArray(Util.getBytesFromHexString(streamMuxConfig));
checkArgument(config.readBits(1) == 0, "Only supports audio mux version 0.");
checkArgument(config.readBits(1) == 1, "Only supports allStreamsSameTimeFraming.");
config.skipBits(6);
checkArgument(config.readBits(4) == 0, "Only supports one program.");
checkArgument(config.readBits(3) == 0, "Only supports one numLayer.");
try {
return AacUtil.parseAudioSpecificConfig(config, false);
} catch (ParserException e) {
throw new IllegalArgumentException(e);
}
}

private static void processMPEG4FmtpAttribute(
Format.Builder formatBuilder, ImmutableMap<String, String> fmtpAttributes) {
@Nullable String configInput = fmtpAttributes.get(PARAMETER_MP4V_CONFIG);
@Nullable String configInput = fmtpAttributes.get(PARAMETER_MP4A_CONFIG);
if (configInput != null) {
byte[] configBuffer = Util.getBytesFromHexString(configInput);
formatBuilder.setInitializationData(ImmutableList.of(configBuffer));
Expand Down
Expand Up @@ -33,7 +33,11 @@ public RtpPayloadReader createPayloadReader(RtpPayloadFormat payloadFormat) {
case MimeTypes.AUDIO_AC3:
return new RtpAc3Reader(payloadFormat);
case MimeTypes.AUDIO_AAC:
return new RtpAacReader(payloadFormat);
if (payloadFormat.mediaEncoding.equals(RtpPayloadFormat.RTP_MEDIA_MPEG4_LATM_AUDIO)) {
return new RtpMp4aReader(payloadFormat);
} else {
return new RtpAacReader(payloadFormat);
}
case MimeTypes.AUDIO_AMR_NB:
case MimeTypes.AUDIO_AMR_WB:
return new RtpAmrReader(payloadFormat);
Expand Down
@@ -0,0 +1,180 @@
/*
* Copyright 2022 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.source.rtsp.reader;

import static com.google.android.exoplayer2.source.rtsp.reader.RtpReaderUtils.toSampleTimeUs;
import static com.google.android.exoplayer2.util.Assertions.checkArgument;
import static com.google.android.exoplayer2.util.Assertions.checkNotNull;
import static com.google.android.exoplayer2.util.Assertions.checkState;
import static com.google.android.exoplayer2.util.Assertions.checkStateNotNull;
import static com.google.android.exoplayer2.util.Util.castNonNull;

import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ParserException;
import com.google.android.exoplayer2.extractor.ExtractorOutput;
import com.google.android.exoplayer2.extractor.TrackOutput;
import com.google.android.exoplayer2.source.rtsp.RtpPacket;
import com.google.android.exoplayer2.source.rtsp.RtpPayloadFormat;
import com.google.android.exoplayer2.util.ParsableBitArray;
import com.google.android.exoplayer2.util.ParsableByteArray;
import com.google.android.exoplayer2.util.Util;
import com.google.common.collect.ImmutableMap;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;

/**
* Parses an MP4A-LATM byte stream carried on RTP packets, and extracts MP4A-LATM Access Units.
*
* <p>Refer to RFC3016 for more details. The LATM byte stream format is defined in ISO/IEC14496-3.
*/
/* package */ final class RtpMp4aReader implements RtpPayloadReader {
private static final String TAG = "RtpMp4aReader";

private static final String PARAMETER_MP4A_CONFIG = "config";

private final RtpPayloadFormat payloadFormat;
private final int numberOfSubframes;
private @MonotonicNonNull TrackOutput trackOutput;
private long firstReceivedTimestamp;
private int previousSequenceNumber;
/** The combined size of a sample that is fragmented into multiple subFrames. */
private int fragmentedSampleSizeBytes;

private long startTimeOffsetUs;
private long fragmentedSampleTimeUs;

/**
* Creates an instance.
*
* @throws IllegalArgumentException If {@link RtpPayloadFormat payloadFormat} is malformed.
*/
public RtpMp4aReader(RtpPayloadFormat payloadFormat) {
this.payloadFormat = payloadFormat;
try {
numberOfSubframes = getNumOfSubframesFromMpeg4AudioConfig(payloadFormat.fmtpParameters);
} catch (ParserException e) {
throw new IllegalArgumentException(e);
}
firstReceivedTimestamp = C.TIME_UNSET;
previousSequenceNumber = C.INDEX_UNSET;
fragmentedSampleSizeBytes = 0;
// The start time offset must be 0 until the first seek.
startTimeOffsetUs = 0;
fragmentedSampleTimeUs = C.TIME_UNSET;
}

@Override
public void createTracks(ExtractorOutput extractorOutput, int trackId) {
trackOutput = extractorOutput.track(trackId, C.TRACK_TYPE_VIDEO);
castNonNull(trackOutput).format(payloadFormat.format);
}

@Override
public void onReceivingFirstPacket(long timestamp, int sequenceNumber) {
checkState(firstReceivedTimestamp == C.TIME_UNSET);
firstReceivedTimestamp = timestamp;
}

@Override
public void consume(
ParsableByteArray data, long timestamp, int sequenceNumber, boolean rtpMarker) {
checkStateNotNull(trackOutput);

int expectedSequenceNumber = RtpPacket.getNextSequenceNumber(previousSequenceNumber);
if (fragmentedSampleSizeBytes > 0 && expectedSequenceNumber < sequenceNumber) {
outputSampleMetadataForFragmentedPackets();
}

for (int subFrameIndex = 0; subFrameIndex < numberOfSubframes; subFrameIndex++) {
int sampleLength = 0;
// Implements PayloadLengthInfo() in ISO/IEC14496-3 Chapter 1.7.3.1, it only supports one
// program and one layer. Each subframe starts with a variable length encoding.
while (data.getPosition() < data.limit()) {
int payloadMuxLength = data.readUnsignedByte();
sampleLength += payloadMuxLength;
if (payloadMuxLength != 0xff) {
break;
}
}

trackOutput.sampleData(data, sampleLength);
fragmentedSampleSizeBytes += sampleLength;
}
fragmentedSampleTimeUs =
toSampleTimeUs(
startTimeOffsetUs, timestamp, firstReceivedTimestamp, payloadFormat.clockRate);
if (rtpMarker) {
outputSampleMetadataForFragmentedPackets();
}
previousSequenceNumber = sequenceNumber;
}

@Override
public void seek(long nextRtpTimestamp, long timeUs) {
firstReceivedTimestamp = nextRtpTimestamp;
fragmentedSampleSizeBytes = 0;
startTimeOffsetUs = timeUs;
}

// Internal methods.

/**
* Parses an MPEG-4 Audio Stream Mux configuration, as defined in ISO/IEC14496-3.
*
* <p>FMTP attribute {@code config} contains the MPEG-4 Audio Stream Mux configuration.
*
* @param fmtpAttributes The format parameters, mapped from the SDP FMTP attribute.
* @return The number of subframes that is carried in each RTP packet.
*/
private static int getNumOfSubframesFromMpeg4AudioConfig(
ImmutableMap<String, String> fmtpAttributes) throws ParserException {
@Nullable String configInput = fmtpAttributes.get(PARAMETER_MP4A_CONFIG);
int numberOfSubframes = 0;
if (configInput != null && configInput.length() % 2 == 0) {
byte[] configBuffer = Util.getBytesFromHexString(configInput);
ParsableBitArray scratchBits = new ParsableBitArray(configBuffer);
int audioMuxVersion = scratchBits.readBits(1);
if (audioMuxVersion == 0) {
checkArgument(scratchBits.readBits(1) == 1, "Only supports allStreamsSameTimeFraming.");
numberOfSubframes = scratchBits.readBits(6);
checkArgument(scratchBits.readBits(4) == 0, "Only suppors one program.");
checkArgument(scratchBits.readBits(3) == 0, "Only suppors one layer.");
} else {
throw ParserException.createForMalformedDataOfUnknownType(
"unsupported audio mux version: " + audioMuxVersion, null);
}
}
// ISO/IEC14496-3 Chapter 1.7.3.2.3: The minimum value is 0 indicating 1 subframe.
return numberOfSubframes + 1;
}

/**
* Outputs sample metadata.
*
* <p>Call this method only after receiving the end of an MPEG4 partition.
*/
private void outputSampleMetadataForFragmentedPackets() {
checkNotNull(trackOutput)
.sampleMetadata(
fragmentedSampleTimeUs,
C.BUFFER_FLAG_KEY_FRAME,
fragmentedSampleSizeBytes,
/* offset= */ 0,
/* cryptoData= */ null);
fragmentedSampleSizeBytes = 0;
fragmentedSampleTimeUs = C.TIME_UNSET;
}
}
Expand Up @@ -70,8 +70,8 @@ public void setUp() throws Exception {
ImmutableList.of(
RtspTestUtils.readRtpPacketStreamDump("media/rtsp/h264-dump.json"),
RtspTestUtils.readRtpPacketStreamDump("media/rtsp/aac-dump.json"),
// MP4A-LATM is not supported at the moment.
RtspTestUtils.readRtpPacketStreamDump("media/rtsp/mp4a-latm-dump.json"));
// MPEG2TS is not supported at the moment.
RtspTestUtils.readRtpPacketStreamDump("media/rtsp/mpeg2ts-dump.json"));
}

@After
Expand Down

0 comments on commit fd2ba37

Please sign in to comment.