blob: a4de910840c0ea1677444b5c3383b7fd33097240 [file] [log] [blame]
/*
* Copyright (C) 2020 Apple Inc. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
* THE POSSIBILITY OF SUCH DAMAGE.
*/
#include "config.h"
#include "SourceBufferParserWebM.h"
#if ENABLE(MEDIA_SOURCE)
#include "AudioTrackPrivateWebM.h"
#include "CMUtilities.h"
#include "ContentType.h"
#include "InbandTextTrackPrivate.h"
#include "Logging.h"
#include "MediaDescription.h"
#include "MediaSampleAVFObjC.h"
#include "NotImplemented.h"
#include "PlatformMediaSessionManager.h"
#include "VP9UtilitiesCocoa.h"
#include "VideoTrackPrivateWebM.h"
#include "WebMAudioUtilitiesCocoa.h"
#include <webm/webm_parser.h>
#include <wtf/Algorithms.h>
#include <wtf/LoggerHelper.h>
#include <wtf/StdLibExtras.h>
#include <wtf/StdList.h>
#include <wtf/darwin/WeakLinking.h>
#include <wtf/spi/darwin/OSVariantSPI.h>
#include "CoreVideoSoftLink.h"
WTF_WEAK_LINK_FORCE_IMPORT(webm::swap);
namespace WTF {
template<typename> struct LogArgument;
template<> struct LogArgument<webm::TrackType> {
static ASCIILiteral toString(webm::TrackType type)
{
switch (type) {
case webm::TrackType::kVideo: return "Video"_s;
case webm::TrackType::kAudio: return "Audio"_s;
case webm::TrackType::kComplex: return "Complex"_s;
case webm::TrackType::kLogo: return "Logo"_s;
case webm::TrackType::kSubtitle: return "Subtitle"_s;
case webm::TrackType::kButtons: return "Buttons"_s;
case webm::TrackType::kControl: return "Control"_s;
}
return "Unknown"_s;
}
};
template<> struct LogArgument<webm::Id> {
static ASCIILiteral toString(webm::Id id)
{
switch (id) {
case webm::Id::kEbml: return "Ebml"_s;
case webm::Id::kEbmlVersion: return "EbmlVersion"_s;
case webm::Id::kEbmlReadVersion: return "EbmlReadVersion"_s;
case webm::Id::kEbmlMaxIdLength: return "EbmlMaxIdLength"_s;
case webm::Id::kEbmlMaxSizeLength: return "EbmlMaxSizeLength"_s;
case webm::Id::kDocType: return "DocType"_s;
case webm::Id::kDocTypeVersion: return "DocTypeVersion"_s;
case webm::Id::kDocTypeReadVersion: return "DocTypeReadVersion"_s;
case webm::Id::kVoid: return "Void"_s;
case webm::Id::kSegment: return "Segment"_s;
case webm::Id::kSeekHead: return "SeekHead"_s;
case webm::Id::kSeek: return "Seek"_s;
case webm::Id::kSeekId: return "SeekId"_s;
case webm::Id::kSeekPosition: return "SeekPosition"_s;
case webm::Id::kInfo: return "Info"_s;
case webm::Id::kTimecodeScale: return "TimecodeScale"_s;
case webm::Id::kDuration: return "Duration"_s;
case webm::Id::kDateUtc: return "DateUtc"_s;
case webm::Id::kTitle: return "Title"_s;
case webm::Id::kMuxingApp: return "MuxingApp"_s;
case webm::Id::kWritingApp: return "WritingApp"_s;
case webm::Id::kCluster: return "Cluster"_s;
case webm::Id::kTimecode: return "Timecode"_s;
case webm::Id::kPrevSize: return "PrevSize"_s;
case webm::Id::kSimpleBlock: return "SimpleBlock"_s;
case webm::Id::kBlockGroup: return "BlockGroup"_s;
case webm::Id::kBlock: return "Block"_s;
case webm::Id::kBlockVirtual: return "BlockVirtual"_s;
case webm::Id::kBlockAdditions: return "BlockAdditions"_s;
case webm::Id::kBlockMore: return "BlockMore"_s;
case webm::Id::kBlockAddId: return "BlockAddId"_s;
case webm::Id::kBlockAdditional: return "BlockAdditional"_s;
case webm::Id::kBlockDuration: return "BlockDuration"_s;
case webm::Id::kReferenceBlock: return "ReferenceBlock"_s;
case webm::Id::kDiscardPadding: return "DiscardPadding"_s;
case webm::Id::kSlices: return "Slices"_s;
case webm::Id::kTimeSlice: return "TimeSlice"_s;
case webm::Id::kLaceNumber: return "LaceNumber"_s;
case webm::Id::kTracks: return "Tracks"_s;
case webm::Id::kTrackEntry: return "TrackEntry"_s;
case webm::Id::kTrackNumber: return "TrackNumber"_s;
case webm::Id::kTrackUid: return "TrackUid"_s;
case webm::Id::kTrackType: return "TrackType"_s;
case webm::Id::kFlagEnabled: return "FlagEnabled"_s;
case webm::Id::kFlagDefault: return "FlagDefault"_s;
case webm::Id::kFlagForced: return "FlagForced"_s;
case webm::Id::kFlagLacing: return "FlagLacing"_s;
case webm::Id::kDefaultDuration: return "DefaultDuration"_s;
case webm::Id::kName: return "Name"_s;
case webm::Id::kLanguage: return "Language"_s;
case webm::Id::kCodecId: return "CodecId"_s;
case webm::Id::kCodecPrivate: return "CodecPrivate"_s;
case webm::Id::kCodecName: return "CodecName"_s;
case webm::Id::kCodecDelay: return "CodecDelay"_s;
case webm::Id::kSeekPreRoll: return "SeekPreRoll"_s;
case webm::Id::kVideo: return "Video"_s;
case webm::Id::kFlagInterlaced: return "FlagInterlaced"_s;
case webm::Id::kStereoMode: return "StereoMode"_s;
case webm::Id::kAlphaMode: return "AlphaMode"_s;
case webm::Id::kPixelWidth: return "PixelWidth"_s;
case webm::Id::kPixelHeight: return "PixelHeight"_s;
case webm::Id::kPixelCropBottom: return "PixelCropBottom"_s;
case webm::Id::kPixelCropTop: return "PixelCropTop"_s;
case webm::Id::kPixelCropLeft: return "PixelCropLeft"_s;
case webm::Id::kPixelCropRight: return "PixelCropRight"_s;
case webm::Id::kDisplayWidth: return "DisplayWidth"_s;
case webm::Id::kDisplayHeight: return "DisplayHeight"_s;
case webm::Id::kDisplayUnit: return "DisplayUnit"_s;
case webm::Id::kAspectRatioType: return "AspectRatioType"_s;
case webm::Id::kFrameRate: return "FrameRate"_s;
case webm::Id::kColour: return "Colour"_s;
case webm::Id::kMatrixCoefficients: return "MatrixCoefficients"_s;
case webm::Id::kBitsPerChannel: return "BitsPerChannel"_s;
case webm::Id::kChromaSubsamplingHorz: return "ChromaSubsamplingHorz"_s;
case webm::Id::kChromaSubsamplingVert: return "ChromaSubsamplingVert"_s;
case webm::Id::kCbSubsamplingHorz: return "CbSubsamplingHorz"_s;
case webm::Id::kCbSubsamplingVert: return "CbSubsamplingVert"_s;
case webm::Id::kChromaSitingHorz: return "ChromaSitingHorz"_s;
case webm::Id::kChromaSitingVert: return "ChromaSitingVert"_s;
case webm::Id::kRange: return "Range"_s;
case webm::Id::kTransferCharacteristics: return "TransferCharacteristics"_s;
case webm::Id::kPrimaries: return "Primaries"_s;
case webm::Id::kMaxCll: return "MaxCll"_s;
case webm::Id::kMaxFall: return "MaxFall"_s;
case webm::Id::kMasteringMetadata: return "MasteringMetadata"_s;
case webm::Id::kPrimaryRChromaticityX: return "PrimaryRChromaticityX"_s;
case webm::Id::kPrimaryRChromaticityY: return "PrimaryRChromaticityY"_s;
case webm::Id::kPrimaryGChromaticityX: return "PrimaryGChromaticityX"_s;
case webm::Id::kPrimaryGChromaticityY: return "PrimaryGChromaticityY"_s;
case webm::Id::kPrimaryBChromaticityX: return "PrimaryBChromaticityX"_s;
case webm::Id::kPrimaryBChromaticityY: return "PrimaryBChromaticityY"_s;
case webm::Id::kWhitePointChromaticityX: return "WhitePointChromaticityX"_s;
case webm::Id::kWhitePointChromaticityY: return "WhitePointChromaticityY"_s;
case webm::Id::kLuminanceMax: return "LuminanceMax"_s;
case webm::Id::kLuminanceMin: return "LuminanceMin"_s;
case webm::Id::kProjection: return "Projection"_s;
case webm::Id::kProjectionType: return "ProjectionType"_s;
case webm::Id::kProjectionPrivate: return "ProjectionPrivate"_s;
case webm::Id::kProjectionPoseYaw: return "ProjectionPoseYaw"_s;
case webm::Id::kProjectionPosePitch: return "ProjectionPosePitch"_s;
case webm::Id::kProjectionPoseRoll: return "ProjectionPoseRoll"_s;
case webm::Id::kAudio: return "Audio"_s;
case webm::Id::kSamplingFrequency: return "SamplingFrequency"_s;
case webm::Id::kOutputSamplingFrequency: return "OutputSamplingFrequency"_s;
case webm::Id::kChannels: return "Channels"_s;
case webm::Id::kBitDepth: return "BitDepth"_s;
case webm::Id::kContentEncodings: return "ContentEncodings"_s;
case webm::Id::kContentEncoding: return "ContentEncoding"_s;
case webm::Id::kContentEncodingOrder: return "ContentEncodingOrder"_s;
case webm::Id::kContentEncodingScope: return "ContentEncodingScope"_s;
case webm::Id::kContentEncodingType: return "ContentEncodingType"_s;
case webm::Id::kContentEncryption: return "ContentEncryption"_s;
case webm::Id::kContentEncAlgo: return "ContentEncAlgo"_s;
case webm::Id::kContentEncKeyId: return "ContentEncKeyId"_s;
case webm::Id::kContentEncAesSettings: return "ContentEncAesSettings"_s;
case webm::Id::kAesSettingsCipherMode: return "AesSettingsCipherMode"_s;
case webm::Id::kCues: return "Cues"_s;
case webm::Id::kCuePoint: return "CuePoint"_s;
case webm::Id::kCueTime: return "CueTime"_s;
case webm::Id::kCueTrackPositions: return "CueTrackPositions"_s;
case webm::Id::kCueTrack: return "CueTrack"_s;
case webm::Id::kCueClusterPosition: return "CueClusterPosition"_s;
case webm::Id::kCueRelativePosition: return "CueRelativePosition"_s;
case webm::Id::kCueDuration: return "CueDuration"_s;
case webm::Id::kCueBlockNumber: return "CueBlockNumber"_s;
case webm::Id::kChapters: return "Chapters"_s;
case webm::Id::kEditionEntry: return "EditionEntry"_s;
case webm::Id::kChapterAtom: return "ChapterAtom"_s;
case webm::Id::kChapterUid: return "ChapterUid"_s;
case webm::Id::kChapterStringUid: return "ChapterStringUid"_s;
case webm::Id::kChapterTimeStart: return "ChapterTimeStart"_s;
case webm::Id::kChapterTimeEnd: return "ChapterTimeEnd"_s;
case webm::Id::kChapterDisplay: return "ChapterDisplay"_s;
case webm::Id::kChapString: return "ChapString"_s;
case webm::Id::kChapLanguage: return "ChapLanguage"_s;
case webm::Id::kChapCountry: return "ChapCountry"_s;
case webm::Id::kTags: return "Tags"_s;
case webm::Id::kTag: return "Tag"_s;
case webm::Id::kTargets: return "Targets"_s;
case webm::Id::kTargetTypeValue: return "TargetTypeValue"_s;
case webm::Id::kTargetType: return "TargetType"_s;
case webm::Id::kTagTrackUid: return "TagTrackUid"_s;
case webm::Id::kSimpleTag: return "SimpleTag"_s;
case webm::Id::kTagName: return "TagName"_s;
case webm::Id::kTagLanguage: return "TagLanguage"_s;
case webm::Id::kTagDefault: return "TagDefault"_s;
case webm::Id::kTagString: return "TagString"_s;
case webm::Id::kTagBinary: return "TagBinary"_s;
}
return "Unknown"_s;
}
};
} // namespace WTF
namespace WebCore {
static WTFLogChannel& logChannel() { return LogMedia; }
static const char* logClassName() { return "SourceBufferParserWebM"; }
// FIXME: Remove this once kCMVideoCodecType_VP9 is added to CMFormatDescription.h
constexpr CMVideoCodecType kCMVideoCodecType_VP9 { 'vp09' };
constexpr uint32_t k_us_in_seconds = 1000000000;
static bool isWebmParserAvailable()
{
return !!webm::swap;
}
using namespace webm;
static Status segmentReadErrorToWebmStatus(SourceBufferParser::Segment::ReadError error)
{
switch (error) {
case SourceBufferParser::Segment::ReadError::EndOfFile: return Status(Status::kEndOfFile);
case SourceBufferParser::Segment::ReadError::FatalError: return Status(Status::Code(WebMParser::ErrorCode::ReaderFailed));
}
}
class WebMParser::SegmentReader final : public webm::Reader {
WTF_MAKE_FAST_ALLOCATED;
public:
void appendSegment(SourceBufferParser::Segment&& segment)
{
m_data.push_back(WTFMove(segment));
if (m_currentSegment == m_data.end())
--m_currentSegment;
}
Status Read(std::size_t numToRead, uint8_t* outputBuffer, uint64_t* numActuallyRead) final
{
ASSERT(outputBuffer && numActuallyRead);
if (!numActuallyRead)
return Status(Status::kNotEnoughMemory);
*numActuallyRead = 0;
if (!outputBuffer)
return Status(Status::kNotEnoughMemory);
while (numToRead && m_currentSegment != m_data.end()) {
auto& currentSegment = *m_currentSegment;
if (m_positionWithinSegment >= currentSegment.size()) {
advanceToNextSegment();
continue;
}
auto readResult = currentSegment.read(m_positionWithinSegment, numToRead, outputBuffer);
if (!readResult.has_value())
return segmentReadErrorToWebmStatus(readResult.error());
auto lastRead = readResult.value();
m_position += lastRead;
*numActuallyRead += lastRead;
m_positionWithinSegment += lastRead;
numToRead -= lastRead;
if (m_positionWithinSegment == currentSegment.size())
advanceToNextSegment();
}
if (!numToRead)
return Status(Status::kOkCompleted);
if (*numActuallyRead)
return Status(Status::kOkPartial);
return Status(Status::kWouldBlock);
}
Status Skip(uint64_t numToSkip, uint64_t* numActuallySkipped) final
{
ASSERT(numActuallySkipped);
if (!numActuallySkipped)
return Status(Status::kNotEnoughMemory);
*numActuallySkipped = 0;
while (m_currentSegment != m_data.end()) {
auto& currentSegment = *m_currentSegment;
if (m_positionWithinSegment >= currentSegment.size()) {
advanceToNextSegment();
continue;
}
size_t numAvailable = currentSegment.size() - m_positionWithinSegment;
if (numToSkip < numAvailable) {
*numActuallySkipped += numToSkip;
m_position += numToSkip;
m_positionWithinSegment += numToSkip;
return Status(Status::kOkCompleted);
}
*numActuallySkipped += numAvailable;
m_position += numAvailable;
m_positionWithinSegment += numAvailable;
numToSkip -= numAvailable;
advanceToNextSegment();
continue;
}
if (!numToSkip)
return Status(Status::kOkCompleted);
if (*numActuallySkipped)
return Status(Status::kOkPartial);
return Status(Status::kWouldBlock);
}
Status ReadInto(std::size_t numToRead, SharedBufferBuilder& outputBuffer, uint64_t* numActuallyRead)
{
ASSERT(numActuallyRead);
if (!numActuallyRead)
return Status(Status::kNotEnoughMemory);
*numActuallyRead = 0;
while (numToRead && m_currentSegment != m_data.end()) {
auto& currentSegment = *m_currentSegment;
if (m_positionWithinSegment >= currentSegment.size()) {
advanceToNextSegment();
continue;
}
RefPtr<SharedBuffer> sharedBuffer = currentSegment.getSharedBuffer();
RefPtr<SharedBuffer> rawBlockBuffer;
if (!sharedBuffer) {
// We could potentially allocate more memory than needed if the read is partial.
Vector<uint8_t> buffer;
if (!buffer.tryReserveInitialCapacity(numToRead))
return Status(Status::kNotEnoughMemory);
buffer.resize(numToRead);
auto readResult = currentSegment.read(m_positionWithinSegment, numToRead, buffer.data());
if (!readResult.has_value())
return segmentReadErrorToWebmStatus(readResult.error());
buffer.resize(readResult.value());
rawBlockBuffer = SharedBuffer::create(WTFMove(buffer));
} else
rawBlockBuffer = sharedBuffer->getContiguousData(m_positionWithinSegment, numToRead);
auto lastRead = rawBlockBuffer->size();
outputBuffer.append(*rawBlockBuffer);
m_position += lastRead;
*numActuallyRead += lastRead;
m_positionWithinSegment += lastRead;
numToRead -= lastRead;
if (m_positionWithinSegment == currentSegment.size())
advanceToNextSegment();
}
if (!numToRead)
return Status(Status::kOkCompleted);
if (*numActuallyRead)
return Status(Status::kOkPartial);
return Status(Status::kWouldBlock);
}
uint64_t Position() const final { return m_position; }
void reset()
{
m_position = 0;
m_data.clear();
m_currentSegment = m_data.end();
}
bool rewindTo(uint64_t rewindToPosition)
{
ASSERT(rewindToPosition <= m_position);
if (rewindToPosition > m_position)
return false;
auto rewindAmount = m_position - rewindToPosition;
while (rewindAmount) {
if (rewindAmount <= m_positionWithinSegment) {
m_positionWithinSegment -= rewindAmount;
return true;
}
if (m_currentSegment == m_data.begin())
return false;
rewindAmount -= m_positionWithinSegment;
--m_currentSegment;
m_positionWithinSegment = m_currentSegment->size();
}
return true;
}
void reclaimSegments()
{
m_data.erase(m_data.begin(), m_currentSegment);
}
private:
void advanceToNextSegment()
{
ASSERT(m_currentSegment != m_data.end());
if (m_currentSegment == m_data.end())
return;
ASSERT(m_positionWithinSegment == m_currentSegment->size());
++m_currentSegment;
m_positionWithinSegment = 0;
}
StdList<SourceBufferParser::Segment> m_data;
StdList<SourceBufferParser::Segment>::iterator m_currentSegment { m_data.end() };
size_t m_position { 0 };
size_t m_positionWithinSegment { 0 };
};
class MediaDescriptionWebM final : public MediaDescription {
WTF_MAKE_FAST_ALLOCATED;
public:
static Ref<MediaDescriptionWebM> create(webm::TrackEntry&& track)
{
ASSERT(isWebmParserAvailable());
return adoptRef(*new MediaDescriptionWebM(WTFMove(track)));
}
MediaDescriptionWebM(webm::TrackEntry&& track)
: m_track { WTFMove(track) }
{
}
AtomString codec() const final
{
if (m_codec)
return *m_codec;
// From: https://www.matroska.org/technical/codec_specs.html
// "Each Codec ID MUST include a Major Codec ID immediately following the Codec ID Prefix.
// A Major Codec ID MAY be followed by an OPTIONAL Codec ID Suffix to communicate a refinement
// of the Major Codec ID. If a Codec ID Suffix is used, then the Codec ID MUST include a forward
// slash (“/”) as a separator between the Major Codec ID and the Codec ID Suffix. The Major Codec
// ID MUST be composed of only capital letters (A-Z) and numbers (0-9). The Codec ID Suffix MUST
// be composed of only capital letters (A-Z), numbers (0-9), underscore (“_”), and forward slash (“/”)."
if (!m_track.codec_id.is_present()) {
m_codec = emptyAtom();
return *m_codec;
}
StringView codecID { m_track.codec_id.value().data(), (unsigned)m_track.codec_id.value().length() };
if (!codecID.startsWith("V_"_s) && !codecID.startsWith("A_"_s) && !codecID.startsWith("S_"_s)) {
m_codec = emptyAtom();
return *m_codec;
}
auto slashLocation = codecID.find('/');
auto length = slashLocation == notFound ? codecID.length() - 2 : slashLocation - 2;
m_codec = codecID.substring(2, length).convertToASCIILowercaseAtom();
return *m_codec;
}
bool isVideo() const final { return m_track.track_type.is_present() && m_track.track_type.value() == TrackType::kVideo; }
bool isAudio() const final { return m_track.track_type.is_present() && m_track.track_type.value() == TrackType::kAudio; }
bool isText() const final { return m_track.track_type.is_present() && m_track.track_type.value() == TrackType::kSubtitle; }
const webm::TrackEntry& track() { return m_track; }
private:
mutable std::optional<AtomString> m_codec;
webm::TrackEntry m_track;
};
Span<const ASCIILiteral> SourceBufferParserWebM::supportedMIMETypes()
{
#if !(ENABLE(VP9) || ENABLE(VORBIS) || ENABLE(OPUS))
return { };
#else
static constexpr std::array types {
#if ENABLE(VP9)
"video/webm"_s,
#endif
#if ENABLE(VORBIS) || ENABLE(OPUS)
"audio/webm"_s,
#endif
};
return types;
#endif
}
static bool canLoadFormatReader()
{
#if !ENABLE(WEBM_FORMAT_READER)
return false;
#elif USE(APPLE_INTERNAL_SDK)
return true;
#else
// FIXME (rdar://72320419): If WebKit was built with ad-hoc code-signing,
// CoreMedia will only load the format reader plug-in when a user default
// is set on Apple internal OSs. That means we cannot currently support WebM
// in public SDK builds on customer OSs.
static bool allowsInternalSecurityPolicies = os_variant_allows_internal_security_policies("com.apple.WebKit");
return allowsInternalSecurityPolicies;
#endif // !USE(APPLE_INTERNAL_SDK)
}
WebMParser::WebMParser(Callback& callback)
: m_parser(makeUniqueWithoutFastMallocCheck<WebmParser>())
, m_reader(makeUniqueRef<SegmentReader>())
, m_callback(callback)
{
}
WebMParser::~WebMParser() = default;
void WebMParser::resetState()
{
INFO_LOG_IF_POSSIBLE(LOGIDENTIFIER);
if (m_parser)
m_parser->DidSeek();
m_reader->reset();
m_state = m_initializationSegmentProcessed ? State::ReadingSegment : State::None;
m_initializationSegment = nullptr;
m_initializationSegmentEncountered = false;
m_currentBlock.reset();
for (auto& track : m_tracks)
track->reset();
}
void WebMParser::reset()
{
m_reader->reset();
m_parser->DidSeek();
}
void WebMParser::createByteRangeSamples()
{
for (auto& track : m_tracks)
track->createByteRangeSamples();
m_createByteRangeSamples = true;
}
ExceptionOr<int> WebMParser::parse(SourceBufferParser::Segment&& segment)
{
if (!m_parser)
return Exception { InvalidStateError };
m_reader->appendSegment(WTFMove(segment));
while (true) {
m_status = m_parser->Feed(this, &m_reader);
if (m_status.ok() || m_status.code == Status::kEndOfFile || m_status.code == Status::kWouldBlock) {
m_reader->reclaimSegments();
// We always keep one sample queued in order to calculate the video sample's time, return it now.
flushPendingVideoSamples();
return 0;
}
if (m_status.code != static_cast<int32_t>(ErrorCode::ReceivedEbmlInsideSegment))
break;
// The WebM Byte Stream Format <https://w3c.github.io/media-source/webm-byte-stream-format.html>
// states that an "Initialization Segment" starts with an Ebml Element followed by a Segment Element,
// and that a "Media Segment" is a single Cluster Element. However, since Cluster Elements are contained
// within a Segment Element, this means that a new Ebml Element can be appended at any time while the
// parser is still parsing children of a Segment Element. In this scenario, "rewind" the reader to the
// position of the incoming Ebml Element, and reset the parser, which will cause the Ebml element to be
// parsed as a top-level element, rather than as a child of the Segment.
if (!m_reader->rewindTo(*m_rewindToPosition)) {
ERROR_LOG_IF_POSSIBLE(LOGIDENTIFIER, "failed to rewind reader");
break;
}
ALWAYS_LOG_IF_POSSIBLE(LOGIDENTIFIER, "received Ebml element while parsing Segment element. Rewound reader and reset parser, retrying");
m_rewindToPosition = std::nullopt;
m_parser->DidSeek();
m_state = State::None;
continue;
}
return m_status.code;
}
void WebMParser::setLogger(const Logger& logger, const void* logIdentifier)
{
m_logger = &logger;
m_logIdentifier = logIdentifier;
}
void WebMParser::invalidate()
{
m_parser = nullptr;
m_tracks.clear();
m_initializationSegment = nullptr;
m_currentBlock.reset();
}
auto WebMParser::trackDataForTrackNumber(uint64_t trackNumber) -> TrackData*
{
for (auto& track : m_tracks) {
if (track->track().track_number.is_present() && track->track().track_number.value() == trackNumber)
return &track;
}
return nullptr;
}
Status WebMParser::OnElementBegin(const ElementMetadata& metadata, Action* action)
{
ASSERT(action);
if (!action)
return Status(Status::kNotEnoughMemory);
if (m_state == State::ReadingSegment && metadata.id == Id::kEbml) {
m_rewindToPosition = metadata.position;
return Status(Status::Code(ErrorCode::ReceivedEbmlInsideSegment));
}
if ((m_state == State::None && metadata.id != Id::kEbml && metadata.id != Id::kSegment)
|| (m_state == State::ReadingSegment && metadata.id != Id::kInfo && metadata.id != Id::kTracks && metadata.id != Id::kCluster)) {
INFO_LOG_IF_POSSIBLE(LOGIDENTIFIER, "state(", m_state, "), id(", metadata.id, "), position(", metadata.position, "), headerSize(", metadata.header_size, "), size(", metadata.size, "), skipping");
*action = Action::kSkip;
return Status(Status::kOkCompleted);
}
auto oldState = m_state;
if (metadata.id == Id::kEbml)
m_state = State::ReadingEbml;
else if (metadata.id == Id::kSegment)
m_state = State::ReadingSegment;
else if (metadata.id == Id::kInfo)
m_state = State::ReadingInfo;
else if (metadata.id == Id::kTracks)
m_state = State::ReadingTracks;
else if (metadata.id == Id::kTrackEntry)
m_state = State::ReadingTrack;
else if (metadata.id == Id::kCluster)
m_state = State::ReadingCluster;
INFO_LOG_IF_POSSIBLE(LOGIDENTIFIER, "state(", oldState, "->", m_state, "), id(", metadata.id, "), position(", metadata.position, "), headerSize(", metadata.header_size, "), size(", metadata.size, ")");
// Apply some sanity check; libwebm::StringParser will read the content into a std::string and ByteParser into a std::vector
std::optional<size_t> maxElementSizeAllowed;
switch (metadata.id) {
case Id::kChapterStringUid:
case Id::kChapString:
case Id::kChapLanguage:
case Id::kChapCountry:
case Id::kDocType:
case Id::kTitle:
case Id::kMuxingApp:
case Id::kWritingApp:
case Id::kTagName:
case Id::kTagLanguage:
case Id::kTagString:
case Id::kTargetType:
case Id::kName:
case Id::kLanguage:
case Id::kCodecId:
case Id::kCodecName:
maxElementSizeAllowed = 1 * 1024 * 1024; // 1MiB
break;
case Id::kBlockAdditional:
case Id::kContentEncKeyId:
case Id::kProjectionPrivate:
case Id::kTagBinary:
maxElementSizeAllowed = 16 * 1024 * 1024; // 16MiB
break;
default:
break;
}
if (maxElementSizeAllowed && metadata.size >= *maxElementSizeAllowed)
return Status(Status::kNotEnoughMemory);
return Status(Status::kOkCompleted);
}
Status WebMParser::OnElementEnd(const ElementMetadata& metadata)
{
INFO_LOG_IF_POSSIBLE(LOGIDENTIFIER);
auto oldState = m_state;
if (metadata.id == Id::kEbml || metadata.id == Id::kSegment)
m_state = State::None;
else if (metadata.id == Id::kInfo || metadata.id == Id::kTracks || metadata.id == Id::kCluster)
m_state = State::ReadingSegment;
else if (metadata.id == Id::kTrackEntry)
m_state = State::ReadingTracks;
INFO_LOG_IF_POSSIBLE(LOGIDENTIFIER, "state(", oldState, "->", m_state, "), id(", metadata.id, "), size(", metadata.size, ")");
if (metadata.id == Id::kTracks) {
if (!m_keyIds.isEmpty() && !m_callback.canDecrypt()) {
ERROR_LOG_IF_POSSIBLE(LOGIDENTIFIER, "Encountered encrypted content without an key request callback");
return Status(Status::Code(ErrorCode::ContentEncrypted));
}
if (m_initializationSegmentEncountered)
m_callback.parsedInitializationData(WTFMove(*m_initializationSegment));
m_initializationSegmentEncountered = false;
m_initializationSegment = nullptr;
m_initializationSegmentProcessed = true;
if (!m_keyIds.isEmpty()) {
for (auto& keyIdPair : m_keyIds)
m_callback.contentKeyRequestInitializationDataForTrackID(WTFMove(keyIdPair.second), keyIdPair.first);
}
m_keyIds.clear();
}
return Status(Status::kOkCompleted);
}
Status WebMParser::OnEbml(const ElementMetadata&, const Ebml& ebml)
{
INFO_LOG_IF_POSSIBLE(LOGIDENTIFIER);
if (ebml.doc_type.is_present() && ebml.doc_type.value().compare("webm"))
return Status(Status::Code(ErrorCode::InvalidDocType));
m_initializationSegmentEncountered = true;
m_initializationSegment = makeUniqueWithoutFastMallocCheck<SourceBufferParser::InitializationSegment>();
// TODO: Setting this to false here, will prevent adding a new media segment should a
// partial init segment be encountered after a call to sourceBuffer.abort().
// It's probably fine as no-one in their right mind should send partial init segment only
// to immediately abort it. We do it this way mostly to avoid getting into a rabbit hole
// of ensuring that libwebm does something sane with rubbish input.
m_initializationSegmentProcessed = false;
return Status(Status::kOkCompleted);
}
Status WebMParser::OnSegmentBegin(const ElementMetadata&, Action* action)
{
INFO_LOG_IF_POSSIBLE(LOGIDENTIFIER);
if (!m_initializationSegmentEncountered) {
ERROR_LOG_IF_POSSIBLE(LOGIDENTIFIER, "Encountered Segment before Embl");
return Status(Status::Code(ErrorCode::InvalidInitSegment));
}
ASSERT(action);
if (!action)
return Status(Status::kNotEnoughMemory);
*action = Action::kRead;
return Status(Status::kOkCompleted);
}
Status WebMParser::OnInfo(const ElementMetadata&, const Info& info)
{
INFO_LOG_IF_POSSIBLE(LOGIDENTIFIER);
if (!m_initializationSegmentEncountered || !m_initializationSegment) {
ERROR_LOG_IF_POSSIBLE(LOGIDENTIFIER, "Encountered Info outside Segment");
return Status(Status::Code(ErrorCode::InvalidInitSegment));
}
auto timecodeScale = info.timecode_scale.is_present() ? info.timecode_scale.value() : 1000000;
m_timescale = k_us_in_seconds / timecodeScale;
m_initializationSegment->duration = info.duration.is_present() ? MediaTime(info.duration.value(), m_timescale) : MediaTime::indefiniteTime();
return Status(Status::kOkCompleted);
}
Status WebMParser::OnClusterBegin(const ElementMetadata&, const Cluster& cluster, Action* action)
{
INFO_LOG_IF_POSSIBLE(LOGIDENTIFIER);
ASSERT(action);
if (!action)
return Status(Status::kNotEnoughMemory);
if (cluster.timecode.is_present())
m_currentTimecode = cluster.timecode.value();
*action = Action::kRead;
return Status(Status::kOkCompleted);
}
Status WebMParser::OnTrackEntry(const ElementMetadata&, const TrackEntry& trackEntry)
{
if (!trackEntry.track_type.is_present() || !trackEntry.codec_id.is_present())
return Status(Status::kOkCompleted);
auto trackType = trackEntry.track_type.value();
String codecId { trackEntry.codec_id.value().data(), (unsigned)trackEntry.codec_id.value().length() };
ALWAYS_LOG_IF_POSSIBLE(LOGIDENTIFIER, trackType, ", codec ", codecId);
if (trackType == TrackType::kVideo && !isSupportedVideoCodec(codecId)) {
ERROR_LOG_IF_POSSIBLE(LOGIDENTIFIER, "Encountered unsupported video codec ID ", codecId);
return Status(Status::Code(ErrorCode::UnsupportedVideoCodec));
}
if (trackType == TrackType::kAudio && !isSupportedAudioCodec(codecId)) {
ERROR_LOG_IF_POSSIBLE(LOGIDENTIFIER, "Encountered unsupported audio codec ID ", codecId);
return Status(Status::Code(ErrorCode::UnsupportedAudioCodec));
}
if (trackType == TrackType::kVideo) {
auto track = VideoTrackPrivateWebM::create(TrackEntry(trackEntry));
if (m_logger)
track->setLogger(*m_logger, LoggerHelper::childLogIdentifier(m_logIdentifier, ++m_nextChildIdentifier));
m_initializationSegment->videoTracks.append({ MediaDescriptionWebM::create(TrackEntry(trackEntry)), WTFMove(track) });
} else if (trackType == TrackType::kAudio) {
auto track = AudioTrackPrivateWebM::create(TrackEntry(trackEntry));
if (m_logger)
track->setLogger(*m_logger, LoggerHelper::childLogIdentifier(m_logIdentifier, ++m_nextChildIdentifier));
m_initializationSegment->audioTracks.append({ MediaDescriptionWebM::create(TrackEntry(trackEntry)), WTFMove(track) });
}
if (trackEntry.content_encodings.is_present() && !trackEntry.content_encodings.value().encodings.empty()) {
ALWAYS_LOG_IF_POSSIBLE(LOGIDENTIFIER, "content_encodings detected:");
for (auto& encoding : trackEntry.content_encodings.value().encodings) {
if (!encoding.is_present())
continue;
auto& encryption = encoding.value().encryption;
if (!encryption.is_present())
continue;
auto& keyIdElement = encryption.value().key_id;
if (!keyIdElement.is_present())
continue;
auto& keyId = keyIdElement.value();
m_keyIds.append(std::make_pair(trackEntry.track_uid.value(), SharedBuffer::create(keyId.data(), keyId.size())));
}
}
StringView codecString { trackEntry.codec_id.value().data(), (unsigned)trackEntry.codec_id.value().length() };
auto track = [&]() -> UniqueRef<TrackData> {
#if ENABLE(VP9)
if (codecString == "V_VP9"_s && isVP9DecoderAvailable())
return VideoTrackData::create(CodecType::VP9, trackEntry, *this);
if (codecString == "V_VP8"_s && isVP8DecoderAvailable())
return VideoTrackData::create(CodecType::VP8, trackEntry, *this);
#endif
#if ENABLE(VORBIS)
if (codecString == "A_VORBIS"_s && isVorbisDecoderAvailable())
return AudioTrackData::create(CodecType::Vorbis, trackEntry, *this);
#endif
#if ENABLE(OPUS)
if (codecString == "A_OPUS"_s && isOpusDecoderAvailable())
return AudioTrackData::create(CodecType::Opus, trackEntry, *this);
#endif
return TrackData::create(CodecType::Unsupported, trackEntry, *this);
}();
if (m_createByteRangeSamples)
track->createByteRangeSamples();
m_tracks.append(WTFMove(track));
return Status(Status::kOkCompleted);
}
webm::Status WebMParser::OnBlockBegin(const ElementMetadata&, const Block& block, Action* action)
{
INFO_LOG_IF_POSSIBLE(LOGIDENTIFIER);
ASSERT(action);
if (!action)
return Status(Status::kNotEnoughMemory);
*action = Action::kRead;
m_currentBlock = std::make_optional<BlockVariant>(Block(block));
return Status(Status::kOkCompleted);
}
webm::Status WebMParser::OnBlockEnd(const ElementMetadata&, const Block&)
{
INFO_LOG_IF_POSSIBLE(LOGIDENTIFIER);
m_currentBlock = std::nullopt;
return Status(Status::kOkCompleted);
}
webm::Status WebMParser::OnSimpleBlockBegin(const ElementMetadata&, const SimpleBlock& block, Action* action)
{
INFO_LOG_IF_POSSIBLE(LOGIDENTIFIER);
ASSERT(action);
if (!action)
return Status(Status::kNotEnoughMemory);
*action = Action::kRead;
m_currentBlock = std::make_optional<BlockVariant>(SimpleBlock(block));
return Status(Status::kOkCompleted);
}
webm::Status WebMParser::OnSimpleBlockEnd(const ElementMetadata&, const SimpleBlock&)
{
INFO_LOG_IF_POSSIBLE(LOGIDENTIFIER);
m_currentBlock = std::nullopt;
return Status(Status::kOkCompleted);
}
webm::Status WebMParser::OnBlockGroupBegin(const webm::ElementMetadata&, webm::Action* action)
{
INFO_LOG_IF_POSSIBLE(LOGIDENTIFIER);
ASSERT(action);
if (!action)
return Status(Status::kNotEnoughMemory);
*action = Action::kRead;
return Status(Status::kOkCompleted);
}
webm::Status WebMParser::OnBlockGroupEnd(const webm::ElementMetadata&, const webm::BlockGroup& blockGroup)
{
INFO_LOG_IF_POSSIBLE(LOGIDENTIFIER);
if (blockGroup.block.is_present() && blockGroup.discard_padding.is_present()) {
auto trackNumber = blockGroup.block.value().track_number;
auto* trackData = trackDataForTrackNumber(trackNumber);
if (!trackData) {
ERROR_LOG_IF_POSSIBLE(LOGIDENTIFIER, "Ignoring unknown track number ", trackNumber);
return Status(Status::kOkCompleted);
}
if (trackData->track().track_uid.is_present() && blockGroup.discard_padding.value() > 0)
m_callback.parsedTrimmingData(trackData->track().track_uid.value(), MediaTime(blockGroup.discard_padding.value(), k_us_in_seconds));
}
return Status(Status::kOkCompleted);
}
webm::Status WebMParser::OnFrame(const FrameMetadata& metadata, Reader* reader, uint64_t* bytesRemaining)
{
ASSERT(reader);
if (!reader)
return Status(Status::kNotEnoughMemory);
if (!m_currentBlock) {
ERROR_LOG_IF_POSSIBLE(LOGIDENTIFIER, "no current block!");
return Status(Status::kInvalidElementId);
}
auto* block = WTF::switchOn(*m_currentBlock, [](Block& block) {
return &block;
}, [](SimpleBlock& block) -> Block* {
return &block;
});
if (!block)
return Status(Status::kInvalidElementId);
auto trackNumber = block->track_number;
auto* trackData = trackDataForTrackNumber(trackNumber);
if (!trackData) {
ERROR_LOG_IF_POSSIBLE(LOGIDENTIFIER, "Ignoring unknown track number ", trackNumber);
return Status(Status::kInvalidElementId);
}
switch (trackData->codec()) {
case CodecType::VP8:
case CodecType::VP9:
case CodecType::Vorbis:
case CodecType::Opus:
break;
case CodecType::Unsupported:
return Skip(reader, bytesRemaining);
}
return trackData->consumeFrameData(*reader, metadata, bytesRemaining, MediaTime(block->timecode + m_currentTimecode, m_timescale));
}
#define PARSER_LOG_ERROR_IF_POSSIBLE(...) if (parser().loggerPtr()) parser().loggerPtr()->error(logChannel(), Logger::LogSiteIdentifier(logClassName(), __func__, parser().logIdentifier()), __VA_ARGS__)
RefPtr<SharedBuffer> WebMParser::TrackData::contiguousCompleteBlockBuffer(size_t offset, size_t length) const
{
if (offset + length > m_completeBlockBuffer->size())
return nullptr;
return m_completeBlockBuffer->getContiguousData(offset, length);
}
webm::Status WebMParser::TrackData::readFrameData(webm::Reader& reader, const webm::FrameMetadata& metadata, uint64_t* bytesRemaining)
{
if (m_completePacketSize && *m_completePacketSize != metadata.size) {
// The packet's metadata doesn't match the currently pending complete packet; restart.
ASSERT_NOT_REACHED_WITH_MESSAGE("TrackData::readFrameData: webm in nonsensical state");
reset();
}
if (!m_completePacketSize)
m_completePacketSize = metadata.size;
while (*bytesRemaining) {
uint64_t bytesRead;
auto status = static_cast<WebMParser::SegmentReader&>(reader).ReadInto(*bytesRemaining, m_currentBlockBuffer, &bytesRead);
*bytesRemaining -= bytesRead;
m_partialBytesRead += bytesRead;
if (!status.completed_ok())
return status;
}
ASSERT(m_partialBytesRead <= *m_completePacketSize);
if (m_partialBytesRead < *m_completePacketSize)
return webm::Status(webm::Status::kOkPartial);
m_completeBlockBuffer = m_currentBlockBuffer.take();
if (m_useByteRange)
m_completeFrameData = MediaSample::ByteRange { metadata.position, metadata.size };
else
m_completeFrameData = Ref { *m_completeBlockBuffer };
m_completePacketSize = std::nullopt;
m_partialBytesRead = 0;
return webm::Status(webm::Status::kOkCompleted);
}
void WebMParser::flushPendingVideoSamples()
{
for (auto& track : m_tracks) {
if (track->trackType() == TrackInfo::TrackType::Video)
downcast<WebMParser::VideoTrackData>(track.get()).flushPendingSamples();
}
}
void WebMParser::VideoTrackData::resetCompletedFramesState()
{
ASSERT(!m_pendingMediaSamples.size());
TrackData::resetCompletedFramesState();
}
webm::Status WebMParser::VideoTrackData::consumeFrameData(webm::Reader& reader, const FrameMetadata& metadata, uint64_t* bytesRemaining, const MediaTime& presentationTime)
{
#if ENABLE(VP9)
auto status = readFrameData(reader, metadata, bytesRemaining);
if (!status.completed_ok())
return status;
constexpr size_t maxHeaderSize = 32; // The maximum length of a VP9 uncompressed header is 144 bits and 11 bytes for VP8. Round high.
size_t segmentHeaderLength = std::min<size_t>(maxHeaderSize, metadata.size);
auto contiguousBuffer = contiguousCompleteBlockBuffer(0, segmentHeaderLength);
if (!contiguousBuffer) {
PARSER_LOG_ERROR_IF_POSSIBLE("VideoTrackData::consumeFrameData failed to create contiguous data block");
return Skip(&reader, bytesRemaining);
}
const uint8_t* blockBufferData = contiguousBuffer->data();
bool isKey = false;
if (codec() == CodecType::VP9) {
if (!m_headerParser.ParseUncompressedHeader(blockBufferData, segmentHeaderLength))
return Skip(&reader, bytesRemaining);
if (m_headerParser.key()) {
isKey = true;
setFormatDescription(createVideoInfoFromVP9HeaderParser(m_headerParser, track().video.value().colour));
}
} else if (codec() == CodecType::VP8) {
auto header = parseVP8FrameHeader(blockBufferData, segmentHeaderLength);
if (header && header->keyframe) {
isKey = true;
setFormatDescription(createVideoInfoFromVP8Header(*header, track().video.value().colour));
}
}
processPendingMediaSamples(presentationTime);
m_pendingMediaSamples.append({ presentationTime, presentationTime, MediaTime::indefiniteTime(), WTFMove(m_completeFrameData), isKey ? MediaSample::SampleFlags::IsSync : MediaSample::SampleFlags::None });
ASSERT(!*bytesRemaining);
return webm::Status(webm::Status::kOkCompleted);
#else
UNUSED_PARAM(presentationTime);
UNUSED_PARAM(sampleCount);
UNUSED_PARAM(metadata);
return Skip(&reader, bytesRemaining);
#endif
}
void WebMParser::VideoTrackData::processPendingMediaSamples(const MediaTime& presentationTime)
{
// WebM container doesn't contain information about duration; the end time of a frame is the start time of the next.
// Some frames however may have a duration of 0 which typically indicates that they should be decoded but not displayed.
// We group all the samples with the same presentation timestamp within the same final MediaSampleBlock.
if (!m_pendingMediaSamples.size())
return;
auto& lastSample = m_pendingMediaSamples.last();
lastSample.duration = presentationTime - lastSample.presentationTime;
if (presentationTime == lastSample.presentationTime)
return;
MediaTime timeOffset;
MediaTime durationOffset;
while (m_pendingMediaSamples.size()) {
auto sample = m_pendingMediaSamples.takeFirst();
if (timeOffset) {
sample.presentationTime += timeOffset;
sample.decodeTime += timeOffset;
auto usableOffset = std::min(durationOffset, sample.duration);
sample.duration -= usableOffset;
durationOffset -= usableOffset;
}
// The MediaFormatReader is unable to deal with samples having a duration of 0.
// We instead set those samples to have a 1us duration and shift the presentation/decode time
// of the following samples in the block by the same offset.
if (!sample.duration) {
sample.duration = MediaTime(1, k_us_in_seconds);
timeOffset += sample.duration;
durationOffset += sample.duration;
}
m_processedMediaSamples.append(WTFMove(sample));
}
m_lastDuration = m_processedMediaSamples.last().duration;
m_lastPresentationTime = presentationTime;
if (!m_processedMediaSamples.info())
m_processedMediaSamples.setInfo(formatDescription());
drainPendingSamples();
}
void WebMParser::VideoTrackData::flushPendingSamples()
{
// We haven't been able to calculate the duration of the last sample as none will follow.
// We set its duration to the track's default duration, or if not known the time of the last sample processed.
if (!m_pendingMediaSamples.size())
return;
auto track = this->track();
MediaTime presentationTime = m_lastPresentationTime ? *m_lastPresentationTime : m_pendingMediaSamples.first().presentationTime;
MediaTime duration;
if (track.default_duration.is_present())
duration = MediaTime(track.default_duration.value() * presentationTime.timeScale() / k_us_in_seconds, presentationTime.timeScale());
else if (m_lastDuration)
duration = *m_lastDuration;
processPendingMediaSamples(presentationTime + duration);
m_lastPresentationTime.reset();
}
void WebMParser::AudioTrackData::resetCompletedFramesState()
{
mNumFramesInCompleteBlock = 0;
TrackData::resetCompletedFramesState();
}
webm::Status WebMParser::AudioTrackData::consumeFrameData(webm::Reader& reader, const FrameMetadata& metadata, uint64_t* bytesRemaining, const MediaTime& presentationTime)
{
auto status = readFrameData(reader, metadata, bytesRemaining);
if (!status.completed_ok())
return status;
if (!formatDescription()) {
if (!track().codec_private.is_present()) {
PARSER_LOG_ERROR_IF_POSSIBLE("Audio track missing magic cookie");
return Skip(&reader, bytesRemaining);
}
RefPtr<AudioInfo> formatDescription;
auto& privateData = track().codec_private.value();
if (codec() == CodecType::Vorbis)
formatDescription = createVorbisAudioInfo(privateData.size(), privateData.data());
else if (codec() == CodecType::Opus) {
auto contiguousBuffer = contiguousCompleteBlockBuffer(0, kOpusMinimumFrameDataSize);
if (!contiguousBuffer) {
PARSER_LOG_ERROR_IF_POSSIBLE("AudioTrackData::consumeFrameData: unable to create contiguous data block");
return Skip(&reader, bytesRemaining);
}
OpusCookieContents cookieContents;
if (!parseOpusPrivateData(privateData.size(), privateData.data(), contiguousBuffer->size(), contiguousBuffer->data(), cookieContents)) {
PARSER_LOG_ERROR_IF_POSSIBLE("Failed to parse Opus private data");
return Skip(&reader, bytesRemaining);
}
if (!cookieContents.framesPerPacket) {
PARSER_LOG_ERROR_IF_POSSIBLE("Opus private data indicates 0 frames per packet; bailing");
return Skip(&reader, bytesRemaining);
}
m_framesPerPacket = cookieContents.framesPerPacket;
m_frameDuration = cookieContents.frameDuration;
formatDescription = createOpusAudioInfo(cookieContents);
}
if (!formatDescription) {
PARSER_LOG_ERROR_IF_POSSIBLE("Failed to create AudioInfo from audio track header");
return Skip(&reader, bytesRemaining);
}
m_packetDuration = MediaTime(formatDescription->framesPerPacket, formatDescription->rate);
setFormatDescription(formatDescription.releaseNonNull());
} else if (codec() == CodecType::Opus) {
// Opus technically allows the frame duration and frames-per-packet values to change from packet to packet.
// CoreAudio doesn't support ASBD values like these to change on a per-packet basis, so throw an error when
// that kind of variability is encountered.
OpusCookieContents cookieContents;
auto& privateData = track().codec_private.value();
auto contiguousBuffer = contiguousCompleteBlockBuffer(0, kOpusMinimumFrameDataSize);
if (!contiguousBuffer) {
PARSER_LOG_ERROR_IF_POSSIBLE("AudioTrackData::consumeFrameData: unable to create contiguous data block");
return Skip(&reader, bytesRemaining);
}
if (!parseOpusPrivateData(privateData.size(), privateData.data(), contiguousBuffer->size(), contiguousBuffer->data(), cookieContents)
|| cookieContents.framesPerPacket != m_framesPerPacket
|| cookieContents.frameDuration != m_frameDuration) {
PARSER_LOG_ERROR_IF_POSSIBLE("Opus frames-per-packet changed within a track; error");
return Status(Status::Code(ErrorCode::VariableFrameDuration));
}
}
if (!m_processedMediaSamples.info())
m_processedMediaSamples.setInfo(formatDescription());
else if (formatDescription() && *formatDescription() != *m_processedMediaSamples.info())
drainPendingSamples();
m_processedMediaSamples.append({ presentationTime, MediaTime::invalidTime(), m_packetDuration, WTFMove(m_completeFrameData), MediaSample::SampleFlags::IsSync });
drainPendingSamples();
ASSERT(!*bytesRemaining);
return webm::Status(webm::Status::kOkCompleted);
}
bool WebMParser::isSupportedVideoCodec(StringView name)
{
return name == "V_VP8"_s || name == "V_VP9"_s;
}
bool WebMParser::isSupportedAudioCodec(StringView name)
{
return name == "A_VORBIS"_s || name == "A_OPUS"_s;
}
SourceBufferParserWebM::SourceBufferParserWebM()
: m_parser(*this)
{
}
bool SourceBufferParserWebM::isWebMFormatReaderAvailable()
{
return PlatformMediaSessionManager::webMFormatReaderEnabled() && canLoadFormatReader() && isWebmParserAvailable();
}
MediaPlayerEnums::SupportsType SourceBufferParserWebM::isContentTypeSupported(const ContentType& type)
{
#if ENABLE(VP9) || ENABLE(VORBIS) || ENABLE(OPUS)
if (!isWebmParserAvailable())
return MediaPlayerEnums::SupportsType::IsNotSupported;
auto containerType = type.containerType();
bool isAudioContainerType = equalLettersIgnoringASCIICase(containerType, "audio/webm"_s);
bool isVideoContainerType = equalLettersIgnoringASCIICase(containerType, "video/webm"_s);
if (!isAudioContainerType && !isVideoContainerType)
return MediaPlayerEnums::SupportsType::IsNotSupported;
bool isAnyAudioCodecAvailable = false;
#if ENABLE(VORBIS)
isAnyAudioCodecAvailable |= isVorbisDecoderAvailable();
#endif
#if ENABLE(OPUS)
isAnyAudioCodecAvailable |= isOpusDecoderAvailable();
#endif
if (isAudioContainerType && !isAnyAudioCodecAvailable)
return MediaPlayerEnums::SupportsType::IsNotSupported;
bool isAnyCodecAvailable = isAnyAudioCodecAvailable;
#if ENABLE(VP9)
isAnyCodecAvailable |= isVP9DecoderAvailable();
#endif
if (!isAnyCodecAvailable)
return MediaPlayerEnums::SupportsType::IsNotSupported;
auto codecs = type.codecs();
if (codecs.isEmpty())
return MediaPlayerEnums::SupportsType::MayBeSupported;
for (auto& codec : codecs) {
#if ENABLE(VP9)
if (codec.startsWith("vp09"_s) || codec.startsWith("vp08"_s) || equal(codec, "vp8"_s) || equal(codec, "vp9"_s)) {
if (!isVP9DecoderAvailable())
return MediaPlayerEnums::SupportsType::IsNotSupported;
auto codecParameters = parseVPCodecParameters(codec);
if (!codecParameters)
return MediaPlayerEnums::SupportsType::IsNotSupported;
if (!isVPCodecConfigurationRecordSupported(*codecParameters))
return MediaPlayerEnums::SupportsType::IsNotSupported;
continue;
}
#endif // ENABLE(VP9)
#if ENABLE(VORBIS)
if (codec == "vorbis"_s) {
if (!isVorbisDecoderAvailable())
return MediaPlayerEnums::SupportsType::IsNotSupported;
continue;
}
#endif // ENABLE(VORBIS)
#if ENABLE(OPUS)
if (codec == "opus"_s) {
if (!isOpusDecoderAvailable())
return MediaPlayerEnums::SupportsType::IsNotSupported;
continue;
}
#endif // ENABLE(OPUS)
return MediaPlayerEnums::SupportsType::IsNotSupported;
}
return MediaPlayerEnums::SupportsType::IsSupported;
#else
UNUSED_PARAM(type);
return MediaPlayerEnums::SupportsType::IsNotSupported;
#endif // ENABLE(VP9) || ENABLE(VORBIS) || ENABLE(OPUS)
}
RefPtr<SourceBufferParserWebM> SourceBufferParserWebM::create(const ContentType& type)
{
if (isContentTypeSupported(type) != MediaPlayerEnums::SupportsType::IsNotSupported)
return adoptRef(new SourceBufferParserWebM());
return nullptr;
}
void WebMParser::provideMediaData(MediaSamplesBlock&& samples)
{
m_callback.parsedMediaData(WTFMove(samples));
}
void SourceBufferParserWebM::parsedInitializationData(InitializationSegment&& initializationSegment)
{
m_callOnClientThreadCallback([this, protectedThis = Ref { *this }, initializationSegment = WTFMove(initializationSegment)]() mutable {
if (m_didParseInitializationDataCallback)
m_didParseInitializationDataCallback(WTFMove(initializationSegment));
});
}
void SourceBufferParserWebM::parsedMediaData(MediaSamplesBlock&& samplesBlock)
{
if (!samplesBlock.info()) {
ERROR_LOG_IF_POSSIBLE(LOGIDENTIFIER, "No TrackInfo set");
return;
}
RetainPtr<CMFormatDescriptionRef> formatDescription;
if (samplesBlock.isVideo()) {
if (m_videoInfo != samplesBlock.info()) {
m_videoInfo = samplesBlock.info();
m_videoFormatDescription = createFormatDescriptionFromTrackInfo(*samplesBlock.info());
}
formatDescription = m_videoFormatDescription;
} else {
if (m_audioInfo != samplesBlock.info()) {
flushPendingAudioSamples();
m_audioFormatDescription = createFormatDescriptionFromTrackInfo(*samplesBlock.info());
m_audioInfo = samplesBlock.info();
}
formatDescription = m_audioFormatDescription;
}
if (samplesBlock.isVideo()) {
returnSamples(WTFMove(samplesBlock), m_videoFormatDescription.get());
return;
}
// Pack audio if needed.
if (m_queuedAudioSamples.size()) {
auto& lastSample = m_queuedAudioSamples.last();
if (lastSample.duration + lastSample.presentationTime != samplesBlock.first().presentationTime)
flushPendingAudioSamples();
}
for (auto& sample : samplesBlock)
m_queuedAudioDuration += sample.duration;
m_queuedAudioSamples.append(WTFMove(samplesBlock));
if (m_queuedAudioDuration < m_minimumAudioSampleDuration)
return;
flushPendingAudioSamples();
}
void SourceBufferParserWebM::returnSamples(MediaSamplesBlock&& block, CMFormatDescriptionRef description)
{
if (block.isEmpty())
return;
auto expectedBuffer = toCMSampleBuffer(WTFMove(block), description);
if (!expectedBuffer) {
ERROR_LOG_IF_POSSIBLE(LOGIDENTIFIER, "toCMSampleBuffer error:", expectedBuffer.error().data());
return;
}
m_callOnClientThreadCallback([this, protectedThis = Ref { *this }, trackID = block.info()->trackID, sampleBuffer = WTFMove(expectedBuffer.value())] () mutable {
if (!m_didProvideMediaDataCallback)
return;
auto mediaSample = MediaSampleAVFObjC::create(sampleBuffer.get(), trackID);
m_didProvideMediaDataCallback(WTFMove(mediaSample), trackID, emptyString());
});
}
void SourceBufferParserWebM::parsedTrimmingData(uint64_t trackID, const MediaTime& padding)
{
m_callOnClientThreadCallback([this, protectedThis = Ref { *this }, trackID, padding] () {
if (m_didParseTrimmingDataCallback)
m_didParseTrimmingDataCallback(trackID, padding);
});
}
void SourceBufferParserWebM::contentKeyRequestInitializationDataForTrackID(Ref<SharedBuffer>&& keyID, uint64_t trackID)
{
if (m_didProvideContentKeyRequestInitializationDataForTrackIDCallback)
m_didProvideContentKeyRequestInitializationDataForTrackIDCallback(WTFMove(keyID), trackID);
}
void SourceBufferParserWebM::flushPendingAudioSamples()
{
if (!m_audioFormatDescription)
return;
ASSERT(m_audioInfo);
m_queuedAudioSamples.setInfo(m_audioInfo.copyRef());
returnSamples(WTFMove(m_queuedAudioSamples), m_audioFormatDescription.get());
m_queuedAudioSamples = { };
m_queuedAudioDuration = { };
}
void SourceBufferParserWebM::appendData(Segment&& segment, CompletionHandler<void()>&& completionHandler, AppendFlags appendFlags)
{
INFO_LOG_IF_POSSIBLE(LOGIDENTIFIER, "flags(", appendFlags == AppendFlags::Discontinuity ? "Discontinuity" : "", "), size(", segment.size(), ")");
if (appendFlags == AppendFlags::Discontinuity)
m_parser.reset();
auto result = m_parser.parse(WTFMove(segment));
if (result.hasException()) {
completionHandler();
return;
}
if (result.returnValue()) {
ERROR_LOG_IF_POSSIBLE(LOGIDENTIFIER, "status.code(", result.returnValue(), ")");
m_callOnClientThreadCallback([this, protectedThis = Ref { *this }, code = result.returnValue()] {
if (m_didEncounterErrorDuringParsingCallback)
m_didEncounterErrorDuringParsingCallback(code);
});
}
// Audio tracks are grouped into meta-samples of a duration no more than m_minimumSampleDuration.
// But at the end of a file, no more audio data may be incoming, so flush and emit any pending
// audio buffers.
flushPendingAudioSamples();
completionHandler();
}
void SourceBufferParserWebM::flushPendingMediaData()
{
}
void SourceBufferParserWebM::setShouldProvideMediaDataForTrackID(bool, uint64_t)
{
notImplemented();
}
bool SourceBufferParserWebM::shouldProvideMediadataForTrackID(uint64_t)
{
notImplemented();
return false;
}
void SourceBufferParserWebM::invalidate()
{
INFO_LOG_IF_POSSIBLE(LOGIDENTIFIER);
m_parser.invalidate();
}
void SourceBufferParserWebM::setLogger(const Logger& logger, const void* logIdentifier)
{
m_parser.setLogger(logger, logIdentifier);
}
void SourceBufferParserWebM::setMinimumAudioSampleDuration(float duration)
{
m_minimumAudioSampleDuration = MediaTime::createWithFloat(duration);
}
}
#endif // ENABLE(MEDIA_SOURCE)