summaryrefslogtreecommitdiffstats
path: root/dom/media/mp4/MP4Metadata.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'dom/media/mp4/MP4Metadata.cpp')
-rw-r--r--dom/media/mp4/MP4Metadata.cpp507
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