diff options
Diffstat (limited to 'dom/media/mp4/MP4Metadata.cpp')
-rw-r--r-- | dom/media/mp4/MP4Metadata.cpp | 507 |
1 files changed, 507 insertions, 0 deletions
diff --git a/dom/media/mp4/MP4Metadata.cpp b/dom/media/mp4/MP4Metadata.cpp new file mode 100644 index 0000000000..88b4fb5c39 --- /dev/null +++ b/dom/media/mp4/MP4Metadata.cpp @@ -0,0 +1,507 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "mozilla/Assertions.h" +#include "mozilla/CheckedInt.h" +#include "mozilla/EndianUtils.h" +#include "mozilla/Logging.h" +#include "mozilla/RefPtr.h" +#include "mozilla/Telemetry.h" +#include "mozilla/UniquePtr.h" +#include "VideoUtils.h" +#include "MoofParser.h" +#include "MP4Metadata.h" +#include "ByteStream.h" +#include "mp4parse.h" + +#include <limits> +#include <stdint.h> +#include <vector> + +using mozilla::media::TimeUnit; + +namespace mozilla { +LazyLogModule gMP4MetadataLog("MP4Metadata"); + +IndiceWrapper::IndiceWrapper(Mp4parseByteData& aRustIndice) { + mIndice.data = nullptr; + mIndice.length = aRustIndice.length; + mIndice.indices = aRustIndice.indices; +} + +size_t IndiceWrapper::Length() const { return mIndice.length; } + +bool IndiceWrapper::GetIndice(size_t aIndex, + MP4SampleIndex::Indice& aIndice) const { + if (aIndex >= mIndice.length) { + MOZ_LOG(gMP4MetadataLog, LogLevel::Error, ("Index overflow in indice")); + return false; + } + + const Mp4parseIndice* indice = &mIndice.indices[aIndex]; + aIndice.start_offset = indice->start_offset; + aIndice.end_offset = indice->end_offset; + aIndice.start_composition = indice->start_composition; + aIndice.end_composition = indice->end_composition; + aIndice.start_decode = indice->start_decode; + aIndice.sync = indice->sync; + return true; +} + +static const char* TrackTypeToString(mozilla::TrackInfo::TrackType aType) { + switch (aType) { + case mozilla::TrackInfo::kAudioTrack: + return "audio"; + case mozilla::TrackInfo::kVideoTrack: + return "video"; + default: + return "unknown"; + } +} + +bool StreamAdaptor::Read(uint8_t* buffer, uintptr_t size, size_t* bytes_read) { + if (!mOffset.isValid()) { + MOZ_LOG(gMP4MetadataLog, LogLevel::Error, + ("Overflow in source stream offset")); + return false; + } + bool rv = mSource->ReadAt(mOffset.value(), buffer, size, bytes_read); + if (rv) { + mOffset += *bytes_read; + } + return rv; +} + +// Wrapper to allow rust to call our read adaptor. +static intptr_t read_source(uint8_t* buffer, uintptr_t size, void* userdata) { + MOZ_ASSERT(buffer); + MOZ_ASSERT(userdata); + + auto source = reinterpret_cast<StreamAdaptor*>(userdata); + size_t bytes_read = 0; + bool rv = source->Read(buffer, size, &bytes_read); + if (!rv) { + MOZ_LOG(gMP4MetadataLog, LogLevel::Warning, ("Error reading source data")); + return -1; + } + return bytes_read; +} + +MP4Metadata::MP4Metadata(ByteStream* aSource) + : mSource(aSource), mSourceAdaptor(aSource) { + DDLINKCHILD("source", aSource); +} + +MP4Metadata::~MP4Metadata() = default; + +nsresult MP4Metadata::Parse() { + Mp4parseIo io = {read_source, &mSourceAdaptor}; + Mp4parseParser* parser = nullptr; + Mp4parseStatus status = mp4parse_new(&io, &parser); + if (status == MP4PARSE_STATUS_OK && parser) { + mParser.reset(parser); + MOZ_ASSERT(mParser); + } else { + MOZ_ASSERT(!mParser); + MOZ_LOG(gMP4MetadataLog, LogLevel::Debug, + ("Parse failed, return code %d\n", status)); + return status == MP4PARSE_STATUS_OOM ? NS_ERROR_OUT_OF_MEMORY + : NS_ERROR_DOM_MEDIA_METADATA_ERR; + } + + UpdateCrypto(); + + return NS_OK; +} + +void MP4Metadata::UpdateCrypto() { + Mp4parsePsshInfo info = {}; + if (mp4parse_get_pssh_info(mParser.get(), &info) != MP4PARSE_STATUS_OK) { + return; + } + + if (info.data.length == 0) { + return; + } + + mCrypto.Update(info.data.data, info.data.length); +} + +bool TrackTypeEqual(TrackInfo::TrackType aLHS, Mp4parseTrackType aRHS) { + switch (aLHS) { + case TrackInfo::kAudioTrack: + return aRHS == MP4PARSE_TRACK_TYPE_AUDIO; + case TrackInfo::kVideoTrack: + return aRHS == MP4PARSE_TRACK_TYPE_VIDEO; + default: + return false; + } +} + +MP4Metadata::ResultAndTrackCount MP4Metadata::GetNumberTracks( + mozilla::TrackInfo::TrackType aType) const { + uint32_t tracks; + auto rv = mp4parse_get_track_count(mParser.get(), &tracks); + if (rv != MP4PARSE_STATUS_OK) { + MOZ_LOG(gMP4MetadataLog, LogLevel::Warning, + ("rust parser error %d counting tracks", rv)); + return {MediaResult(NS_ERROR_DOM_MEDIA_METADATA_ERR, + RESULT_DETAIL("Rust parser error %d", rv)), + MP4Metadata::NumberTracksError()}; + } + + uint32_t total = 0; + for (uint32_t i = 0; i < tracks; ++i) { + Mp4parseTrackInfo track_info; + rv = mp4parse_get_track_info(mParser.get(), i, &track_info); + if (rv != MP4PARSE_STATUS_OK) { + continue; + } + + if (track_info.track_type == MP4PARSE_TRACK_TYPE_AUDIO) { + Mp4parseTrackAudioInfo audio; + auto rv = mp4parse_get_track_audio_info(mParser.get(), i, &audio); + if (rv != MP4PARSE_STATUS_OK) { + MOZ_LOG(gMP4MetadataLog, LogLevel::Warning, + ("mp4parse_get_track_audio_info returned error %d", rv)); + continue; + } + MOZ_DIAGNOSTIC_ASSERT(audio.sample_info_count > 0, + "Must have at least one audio sample info"); + if (audio.sample_info_count == 0) { + return { + MediaResult( + NS_ERROR_DOM_MEDIA_METADATA_ERR, + RESULT_DETAIL( + "Got 0 audio sample info while checking number tracks")), + MP4Metadata::NumberTracksError()}; + } + // We assume the codec of the first sample info is representative of the + // whole track and skip it if we don't recognize the codec. + if (audio.sample_info[0].codec_type == MP4PARSE_CODEC_UNKNOWN) { + continue; + } + } else if (track_info.track_type == MP4PARSE_TRACK_TYPE_VIDEO) { + Mp4parseTrackVideoInfo video; + auto rv = mp4parse_get_track_video_info(mParser.get(), i, &video); + if (rv != MP4PARSE_STATUS_OK) { + MOZ_LOG(gMP4MetadataLog, LogLevel::Warning, + ("mp4parse_get_track_video_info returned error %d", rv)); + continue; + } + MOZ_DIAGNOSTIC_ASSERT(video.sample_info_count > 0, + "Must have at least one video sample info"); + if (video.sample_info_count == 0) { + return { + MediaResult( + NS_ERROR_DOM_MEDIA_METADATA_ERR, + RESULT_DETAIL( + "Got 0 video sample info while checking number tracks")), + MP4Metadata::NumberTracksError()}; + } + // We assume the codec of the first sample info is representative of the + // whole track and skip it if we don't recognize the codec. + if (video.sample_info[0].codec_type == MP4PARSE_CODEC_UNKNOWN) { + continue; + } + } else { + // Only audio and video are supported + continue; + } + if (TrackTypeEqual(aType, track_info.track_type)) { + total += 1; + } + } + + MOZ_LOG(gMP4MetadataLog, LogLevel::Info, + ("%s tracks found: %u", TrackTypeToString(aType), total)); + + return {NS_OK, total}; +} + +Maybe<uint32_t> MP4Metadata::TrackTypeToGlobalTrackIndex( + mozilla::TrackInfo::TrackType aType, size_t aTrackNumber) const { + uint32_t tracks; + auto rv = mp4parse_get_track_count(mParser.get(), &tracks); + if (rv != MP4PARSE_STATUS_OK) { + return Nothing(); + } + + /* The MP4Metadata API uses a per-TrackType index of tracks, but mp4parse + (and libstagefright) use a global track index. Convert the index by + counting the tracks of the requested type and returning the global + track index when a match is found. */ + uint32_t perType = 0; + for (uint32_t i = 0; i < tracks; ++i) { + Mp4parseTrackInfo track_info; + rv = mp4parse_get_track_info(mParser.get(), i, &track_info); + if (rv != MP4PARSE_STATUS_OK) { + continue; + } + if (TrackTypeEqual(aType, track_info.track_type)) { + if (perType == aTrackNumber) { + return Some(i); + } + perType += 1; + } + } + + return Nothing(); +} + +MP4Metadata::ResultAndTrackInfo MP4Metadata::GetTrackInfo( + mozilla::TrackInfo::TrackType aType, size_t aTrackNumber) const { + Maybe<uint32_t> trackIndex = TrackTypeToGlobalTrackIndex(aType, aTrackNumber); + if (trackIndex.isNothing()) { + return {MediaResult(NS_ERROR_DOM_MEDIA_METADATA_ERR, + RESULT_DETAIL("No %s tracks", TrackTypeToStr(aType))), + nullptr}; + } + + Mp4parseTrackInfo info; + auto rv = mp4parse_get_track_info(mParser.get(), trackIndex.value(), &info); + if (rv != MP4PARSE_STATUS_OK) { + MOZ_LOG(gMP4MetadataLog, LogLevel::Warning, + ("mp4parse_get_track_info returned %d", rv)); + return {MediaResult(NS_ERROR_DOM_MEDIA_METADATA_ERR, + RESULT_DETAIL("Cannot find %s track #%zu", + TrackTypeToStr(aType), aTrackNumber)), + nullptr}; + } +#ifdef DEBUG + bool haveSampleInfo = false; + const char* codecString = "unrecognized"; + Mp4parseCodec codecType = MP4PARSE_CODEC_UNKNOWN; + if (info.track_type == MP4PARSE_TRACK_TYPE_AUDIO) { + Mp4parseTrackAudioInfo audio; + auto rv = mp4parse_get_track_audio_info(mParser.get(), trackIndex.value(), + &audio); + if (rv == MP4PARSE_STATUS_OK && audio.sample_info_count > 0) { + codecType = audio.sample_info[0].codec_type; + haveSampleInfo = true; + } + } else if (info.track_type == MP4PARSE_TRACK_TYPE_VIDEO) { + Mp4parseTrackVideoInfo video; + auto rv = mp4parse_get_track_video_info(mParser.get(), trackIndex.value(), + &video); + if (rv == MP4PARSE_STATUS_OK && video.sample_info_count > 0) { + codecType = video.sample_info[0].codec_type; + haveSampleInfo = true; + } + } + if (haveSampleInfo) { + switch (codecType) { + case MP4PARSE_CODEC_UNKNOWN: + codecString = "unknown"; + break; + case MP4PARSE_CODEC_AAC: + codecString = "aac"; + break; + case MP4PARSE_CODEC_OPUS: + codecString = "opus"; + break; + case MP4PARSE_CODEC_FLAC: + codecString = "flac"; + break; + case MP4PARSE_CODEC_ALAC: + codecString = "alac"; + break; + case MP4PARSE_CODEC_H263: + codecString = "h.263"; + break; + case MP4PARSE_CODEC_AVC: + codecString = "h.264"; + break; + case MP4PARSE_CODEC_VP9: + codecString = "vp9"; + break; + case MP4PARSE_CODEC_AV1: + codecString = "av1"; + break; + case MP4PARSE_CODEC_MP3: + codecString = "mp3"; + break; + case MP4PARSE_CODEC_MP4V: + codecString = "mp4v"; + break; + case MP4PARSE_CODEC_JPEG: + codecString = "jpeg"; + break; + case MP4PARSE_CODEC_AC3: + codecString = "ac-3"; + break; + case MP4PARSE_CODEC_EC3: + codecString = "ec-3"; + break; + } + } + MOZ_LOG(gMP4MetadataLog, LogLevel::Debug, + ("track codec %s (%u)\n", codecString, codecType)); +#endif + + Mp4parseTrackInfo track_info; + rv = mp4parse_get_track_info(mParser.get(), trackIndex.value(), &track_info); + if (rv != MP4PARSE_STATUS_OK) { + MOZ_LOG(gMP4MetadataLog, LogLevel::Warning, + ("mp4parse_get_track_info returned error %d", rv)); + return {MediaResult(NS_ERROR_DOM_MEDIA_METADATA_ERR, + RESULT_DETAIL("Cannot parse %s track #%zu", + TrackTypeToStr(aType), aTrackNumber)), + nullptr}; + } + + uint32_t timeScale = info.time_scale; + + // This specialization interface is wild. + UniquePtr<mozilla::TrackInfo> e; + switch (aType) { + case TrackInfo::TrackType::kAudioTrack: { + Mp4parseTrackAudioInfo audio; + auto rv = mp4parse_get_track_audio_info(mParser.get(), trackIndex.value(), + &audio); + if (rv != MP4PARSE_STATUS_OK) { + MOZ_LOG(gMP4MetadataLog, LogLevel::Warning, + ("mp4parse_get_track_audio_info returned error %d", rv)); + return {MediaResult(NS_ERROR_DOM_MEDIA_METADATA_ERR, + RESULT_DETAIL("Cannot parse %s track #%zu", + TrackTypeToStr(aType), aTrackNumber)), + nullptr}; + } + + auto indices = GetTrackIndice(info.track_id); + if (!indices.Ref()) { + // non fatal + MOZ_LOG(gMP4MetadataLog, LogLevel::Warning, + ("Can't get index table for audio track, duration might be " + "slightly incorrect")); + } + auto track = mozilla::MakeUnique<MP4AudioInfo>(); + MediaResult updateStatus = + track->Update(&info, &audio, indices.Ref().get()); + if (NS_FAILED(updateStatus)) { + MOZ_LOG(gMP4MetadataLog, LogLevel::Warning, + ("Updating audio track failed with %s", + updateStatus.Message().get())); + return {MediaResult(NS_ERROR_DOM_MEDIA_METADATA_ERR, + RESULT_DETAIL( + "Failed to update %s track #%zu with error: %s", + TrackTypeToStr(aType), aTrackNumber, + updateStatus.Message().get())), + nullptr}; + } + e = std::move(track); + } break; + case TrackInfo::TrackType::kVideoTrack: { + Mp4parseTrackVideoInfo video; + auto rv = mp4parse_get_track_video_info(mParser.get(), trackIndex.value(), + &video); + if (rv != MP4PARSE_STATUS_OK) { + MOZ_LOG(gMP4MetadataLog, LogLevel::Warning, + ("mp4parse_get_track_video_info returned error %d", rv)); + return {MediaResult(NS_ERROR_DOM_MEDIA_METADATA_ERR, + RESULT_DETAIL("Cannot parse %s track #%zu", + TrackTypeToStr(aType), aTrackNumber)), + nullptr}; + } + auto track = mozilla::MakeUnique<MP4VideoInfo>(); + MediaResult updateStatus = track->Update(&info, &video); + if (NS_FAILED(updateStatus)) { + MOZ_LOG(gMP4MetadataLog, LogLevel::Warning, + ("Updating video track failed with %s", + updateStatus.Message().get())); + return {MediaResult(NS_ERROR_DOM_MEDIA_METADATA_ERR, + RESULT_DETAIL( + "Failed to update %s track #%zu with error: %s", + TrackTypeToStr(aType), aTrackNumber, + updateStatus.Message().get())), + nullptr}; + } + e = std::move(track); + } break; + default: + MOZ_LOG(gMP4MetadataLog, LogLevel::Warning, + ("unhandled track type %d", aType)); + return {MediaResult(NS_ERROR_DOM_MEDIA_METADATA_ERR, + RESULT_DETAIL("Cannot handle %s track #%zu", + TrackTypeToStr(aType), aTrackNumber)), + nullptr}; + } + + e->mTimeScale = timeScale; + + // No duration in track, use fragment_duration. + if (e && !e->mDuration.IsPositive()) { + Mp4parseFragmentInfo fragmentInfo; + auto rv = mp4parse_get_fragment_info(mParser.get(), &fragmentInfo); + if (rv == MP4PARSE_STATUS_OK) { + // This doesn't use the time scale of the track, but the time scale + // indicated in the mvhd box + e->mDuration = TimeUnit(fragmentInfo.fragment_duration, + AssertedCast<int64_t>(fragmentInfo.time_scale)); + } + } + + if (e && e->IsValid()) { + return {NS_OK, std::move(e)}; + } + MOZ_LOG(gMP4MetadataLog, LogLevel::Debug, ("TrackInfo didn't validate")); + + return {MediaResult(NS_ERROR_DOM_MEDIA_METADATA_ERR, + RESULT_DETAIL("Invalid %s track #%zu", + TrackTypeToStr(aType), aTrackNumber)), + nullptr}; +} + +bool MP4Metadata::CanSeek() const { return true; } + +MP4Metadata::ResultAndCryptoFile MP4Metadata::Crypto() const { + return {NS_OK, &mCrypto}; +} + +MP4Metadata::ResultAndIndice MP4Metadata::GetTrackIndice( + uint32_t aTrackId) const { + Mp4parseByteData indiceRawData = {}; + + uint8_t fragmented = false; + auto rv = mp4parse_is_fragmented(mParser.get(), aTrackId, &fragmented); + if (rv != MP4PARSE_STATUS_OK) { + return {MediaResult(NS_ERROR_DOM_MEDIA_METADATA_ERR, + RESULT_DETAIL("Cannot parse whether track id %u is " + "fragmented, mp4parse_error=%d", + aTrackId, int(rv))), + nullptr}; + } + + if (!fragmented) { + rv = mp4parse_get_indice_table(mParser.get(), aTrackId, &indiceRawData); + if (rv != MP4PARSE_STATUS_OK) { + return { + MediaResult(NS_ERROR_DOM_MEDIA_METADATA_ERR, + RESULT_DETAIL("Cannot parse index table in track id %u, " + "mp4parse_error=%d", + aTrackId, int(rv))), + nullptr}; + } + } + + UniquePtr<IndiceWrapper> indice; + indice = mozilla::MakeUnique<IndiceWrapper>(indiceRawData); + + return {NS_OK, std::move(indice)}; +} + +/*static*/ MP4Metadata::ResultAndByteBuffer MP4Metadata::Metadata( + ByteStream* aSource) { + auto parser = mozilla::MakeUnique<MoofParser>( + aSource, AsVariant(ParseAllTracks{}), false); + RefPtr<mozilla::MediaByteBuffer> buffer = parser->Metadata(); + if (!buffer) { + return {MediaResult(NS_ERROR_DOM_MEDIA_METADATA_ERR, + RESULT_DETAIL("Cannot parse metadata")), + nullptr}; + } + return {NS_OK, std::move(buffer)}; +} + +} // namespace mozilla |