mirror of
https://github.com/LadybirdBrowser/ladybird.git
synced 2025-12-05 01:10:24 +00:00
273 lines
10 KiB
C++
273 lines
10 KiB
C++
/*
|
|
* Copyright (c) 2025, Luke Wilde <luke@ladybird.org>
|
|
* Copyright (c) 2025, Gregory Bertilson <gregory@ladybird.org>
|
|
*
|
|
* SPDX-License-Identifier: BSD-2-Clause
|
|
*/
|
|
|
|
#include <AK/Math.h>
|
|
#include <AK/MemoryStream.h>
|
|
#include <AK/Stream.h>
|
|
#include <AK/Time.h>
|
|
#include <LibMedia/FFmpeg/FFmpegDemuxer.h>
|
|
#include <LibMedia/FFmpeg/FFmpegHelpers.h>
|
|
|
|
extern "C" {
|
|
#include <libavformat/avformat.h>
|
|
}
|
|
|
|
namespace Media::FFmpeg {
|
|
|
|
FFmpegDemuxer::FFmpegDemuxer(ReadonlyBytes data, NonnullOwnPtr<SeekableStream>&& stream, NonnullOwnPtr<Media::FFmpeg::FFmpegIOContext>&& io_context)
|
|
: m_data(data)
|
|
, m_stream(move(stream))
|
|
, m_io_context(move(io_context))
|
|
{
|
|
}
|
|
|
|
FFmpegDemuxer::~FFmpegDemuxer()
|
|
{
|
|
if (m_format_context != nullptr)
|
|
avformat_close_input(&m_format_context);
|
|
}
|
|
|
|
static DecoderErrorOr<void> initialize_format_context(AVFormatContext*& format_context, AVIOContext& io_context)
|
|
{
|
|
format_context = avformat_alloc_context();
|
|
if (format_context == nullptr)
|
|
return DecoderError::with_description(DecoderErrorCategory::Memory, "Failed to allocate format context"sv);
|
|
format_context->pb = &io_context;
|
|
if (avformat_open_input(&format_context, nullptr, nullptr, nullptr) < 0)
|
|
return DecoderError::with_description(DecoderErrorCategory::Corrupted, "Failed to open input for format parsing"sv);
|
|
|
|
// Read stream info; doing this is required for headerless formats like MPEG
|
|
if (avformat_find_stream_info(format_context, nullptr) < 0)
|
|
return DecoderError::with_description(DecoderErrorCategory::Corrupted, "Failed to find stream info"sv);
|
|
|
|
return {};
|
|
}
|
|
|
|
DecoderErrorOr<NonnullRefPtr<FFmpegDemuxer>> FFmpegDemuxer::from_data(ReadonlyBytes data)
|
|
{
|
|
auto stream = DECODER_TRY_ALLOC(try_make<FixedMemoryStream>(data));
|
|
auto io_context = DECODER_TRY_ALLOC(Media::FFmpeg::FFmpegIOContext::create(*stream));
|
|
auto demuxer = DECODER_TRY_ALLOC(adopt_nonnull_ref_or_enomem(new (nothrow) FFmpegDemuxer(data, move(stream), move(io_context))));
|
|
|
|
TRY(initialize_format_context(demuxer->m_format_context, *demuxer->m_io_context->avio_context()));
|
|
|
|
return demuxer;
|
|
}
|
|
|
|
FFmpegDemuxer::TrackContext& FFmpegDemuxer::get_track_context(Track const& track)
|
|
{
|
|
return *m_track_contexts.ensure(track, [&] {
|
|
auto stream = MUST(try_make<FixedMemoryStream>(m_data));
|
|
auto io_context = MUST(Media::FFmpeg::FFmpegIOContext::create(*stream));
|
|
|
|
auto track_context = make<TrackContext>(move(stream), move(io_context));
|
|
|
|
// We've already initialized a format context, so the only way this can fail is OOM.
|
|
MUST(initialize_format_context(track_context->format_context, *track_context->io_context->avio_context()));
|
|
|
|
track_context->packet = av_packet_alloc();
|
|
VERIFY(track_context->packet != nullptr);
|
|
return track_context;
|
|
});
|
|
}
|
|
|
|
static inline AK::Duration time_units_to_duration(i64 time_units, AVRational const& time_base)
|
|
{
|
|
VERIFY(time_base.num > 0);
|
|
VERIFY(time_base.den > 0);
|
|
return AK::Duration::from_time_units(time_units, time_base.num, time_base.den);
|
|
}
|
|
|
|
static inline i64 duration_to_time_units(AK::Duration duration, AVRational const& time_base)
|
|
{
|
|
VERIFY(time_base.num > 0);
|
|
VERIFY(time_base.den > 0);
|
|
return duration.to_time_units(time_base.num, time_base.den);
|
|
}
|
|
|
|
DecoderErrorOr<AK::Duration> FFmpegDemuxer::total_duration()
|
|
{
|
|
if (m_format_context->duration < 0) {
|
|
return DecoderError::format(DecoderErrorCategory::Unknown, "Negative stream duration");
|
|
}
|
|
|
|
return AK::Duration::from_time_units(m_format_context->duration, 1, AV_TIME_BASE);
|
|
}
|
|
|
|
DecoderErrorOr<AK::Duration> FFmpegDemuxer::duration_of_track(Track const& track)
|
|
{
|
|
VERIFY(track.identifier() < m_format_context->nb_streams);
|
|
auto* stream = m_format_context->streams[track.identifier()];
|
|
|
|
if (stream->duration >= 0) {
|
|
return time_units_to_duration(stream->duration, stream->time_base);
|
|
}
|
|
|
|
// If the stream doesn't specify the duration, fallback to what the container says the duration is.
|
|
return total_duration();
|
|
}
|
|
|
|
DecoderErrorOr<Track> FFmpegDemuxer::get_track_for_stream_index(u32 stream_index)
|
|
{
|
|
VERIFY(stream_index < m_format_context->nb_streams);
|
|
|
|
auto& stream = *m_format_context->streams[stream_index];
|
|
auto type = track_type_from_ffmpeg_media_type(stream.codecpar->codec_type);
|
|
auto get_string_metadata = [&](char const* key) {
|
|
auto* name_entry = av_dict_get(stream.metadata, key, nullptr, 0);
|
|
if (name_entry == nullptr)
|
|
return Utf16String();
|
|
return Utf16String::from_utf8(StringView(name_entry->value, strlen(name_entry->value)));
|
|
};
|
|
auto name = get_string_metadata("title");
|
|
auto language = get_string_metadata("language");
|
|
Track track(type, stream_index, name, language);
|
|
|
|
if (type == TrackType::Video) {
|
|
auto color_primaries = static_cast<ColorPrimaries>(stream.codecpar->color_primaries);
|
|
auto transfer_characteristics = static_cast<TransferCharacteristics>(stream.codecpar->color_trc);
|
|
auto matrix_coefficients = static_cast<MatrixCoefficients>(stream.codecpar->color_space);
|
|
auto color_range = [stream] {
|
|
switch (stream.codecpar->color_range) {
|
|
case AVColorRange::AVCOL_RANGE_MPEG:
|
|
return VideoFullRangeFlag::Studio;
|
|
case AVColorRange::AVCOL_RANGE_JPEG:
|
|
return VideoFullRangeFlag::Full;
|
|
default:
|
|
return VideoFullRangeFlag::Unspecified;
|
|
}
|
|
}();
|
|
|
|
track.set_video_data({
|
|
.pixel_width = static_cast<u64>(stream.codecpar->width),
|
|
.pixel_height = static_cast<u64>(stream.codecpar->height),
|
|
.cicp = CodingIndependentCodePoints(color_primaries, transfer_characteristics, matrix_coefficients, color_range),
|
|
});
|
|
}
|
|
|
|
return track;
|
|
}
|
|
|
|
DecoderErrorOr<Vector<Track>> FFmpegDemuxer::get_tracks_for_type(TrackType type)
|
|
{
|
|
auto media_type = ffmpeg_media_type_from_track_type(type);
|
|
Vector<Track> tracks = {};
|
|
for (u32 i = 0; i < m_format_context->nb_streams; i++) {
|
|
auto& stream = *m_format_context->streams[i];
|
|
if (stream.codecpar->codec_type != media_type)
|
|
continue;
|
|
|
|
tracks.append(TRY(get_track_for_stream_index(i)));
|
|
}
|
|
return tracks;
|
|
}
|
|
|
|
DecoderErrorOr<Optional<Track>> FFmpegDemuxer::get_preferred_track_for_type(TrackType type)
|
|
{
|
|
auto media_type = ffmpeg_media_type_from_track_type(type);
|
|
auto best_stream_index = av_find_best_stream(m_format_context, media_type, -1, -1, nullptr, 0);
|
|
if (best_stream_index < 0)
|
|
return OptionalNone();
|
|
|
|
return get_track_for_stream_index(best_stream_index);
|
|
}
|
|
|
|
DecoderErrorOr<DemuxerSeekResult> FFmpegDemuxer::seek_to_most_recent_keyframe(Track const& track, AK::Duration timestamp, DemuxerSeekOptions)
|
|
{
|
|
auto& track_context = get_track_context(track);
|
|
auto& format_context = *track_context.format_context;
|
|
|
|
VERIFY(format_context.nb_streams == m_format_context->nb_streams);
|
|
VERIFY(track.identifier() < format_context.nb_streams);
|
|
auto& stream = *format_context.streams[track.identifier()];
|
|
auto av_timestamp = duration_to_time_units(timestamp, stream.time_base);
|
|
|
|
auto seek_succeeded = false;
|
|
if (track_context.is_seekable && av_seek_frame(&format_context, stream.index, av_timestamp, AVSEEK_FLAG_BACKWARD) >= 0)
|
|
seek_succeeded = true;
|
|
if (!seek_succeeded) {
|
|
track_context.is_seekable = false;
|
|
auto av_base_timestamp = duration_to_time_units(timestamp, AV_TIME_BASE_Q);
|
|
if (av_seek_frame(&format_context, -1, av_base_timestamp, AVSEEK_FLAG_BACKWARD) < 0)
|
|
return DecoderError::format(DecoderErrorCategory::Corrupted, "Failed to seek");
|
|
}
|
|
|
|
return DemuxerSeekResult::MovedPosition;
|
|
}
|
|
|
|
DecoderErrorOr<CodecID> FFmpegDemuxer::get_codec_id_for_track(Track const& track)
|
|
{
|
|
VERIFY(track.identifier() < m_format_context->nb_streams);
|
|
auto* stream = m_format_context->streams[track.identifier()];
|
|
return media_codec_id_from_ffmpeg_codec_id(stream->codecpar->codec_id);
|
|
}
|
|
|
|
DecoderErrorOr<ReadonlyBytes> FFmpegDemuxer::get_codec_initialization_data_for_track(Track const& track)
|
|
{
|
|
VERIFY(track.identifier() < m_format_context->nb_streams);
|
|
auto* stream = m_format_context->streams[track.identifier()];
|
|
return ReadonlyBytes { stream->codecpar->extradata, static_cast<size_t>(stream->codecpar->extradata_size) };
|
|
}
|
|
|
|
DecoderErrorOr<CodedFrame> FFmpegDemuxer::get_next_sample_for_track(Track const& track)
|
|
{
|
|
auto& track_context = get_track_context(track);
|
|
auto& format_context = *track_context.format_context;
|
|
auto& packet = *track_context.packet;
|
|
|
|
VERIFY(format_context.nb_streams == m_format_context->nb_streams);
|
|
VERIFY(track.identifier() < format_context.nb_streams);
|
|
auto& stream = *format_context.streams[track.identifier()];
|
|
|
|
for (;;) {
|
|
auto read_frame_error = av_read_frame(&format_context, &packet);
|
|
if (read_frame_error < 0) {
|
|
if (read_frame_error == AVERROR_EOF)
|
|
return DecoderError::format(DecoderErrorCategory::EndOfStream, "End of stream");
|
|
|
|
return DecoderError::format(DecoderErrorCategory::Unknown, "Failed to read frame");
|
|
}
|
|
if (packet.stream_index != stream.index) {
|
|
av_packet_unref(&packet);
|
|
continue;
|
|
}
|
|
|
|
auto auxiliary_data = [&]() -> CodedFrame::AuxiliaryData {
|
|
if (track.type() == TrackType::Video) {
|
|
return CodedVideoFrameData();
|
|
}
|
|
if (track.type() == TrackType::Audio) {
|
|
return CodedAudioFrameData();
|
|
}
|
|
VERIFY_NOT_REACHED();
|
|
}();
|
|
|
|
// Copy the packet data so that we have a permanent reference to it whilst the Sample is alive, which allows us
|
|
// to wipe the packet afterwards.
|
|
auto packet_data = DECODER_TRY_ALLOC(ByteBuffer::copy(packet.data, packet.size));
|
|
|
|
auto flags = (packet.flags & AV_PKT_FLAG_KEY) != 0 ? FrameFlags::Keyframe : FrameFlags::None;
|
|
auto sample = CodedFrame(
|
|
time_units_to_duration(packet.pts, stream.time_base),
|
|
flags,
|
|
move(packet_data),
|
|
auxiliary_data);
|
|
|
|
// Wipe the packet now that the data is safe.
|
|
av_packet_unref(&packet);
|
|
return sample;
|
|
}
|
|
}
|
|
|
|
FFmpegDemuxer::TrackContext::~TrackContext()
|
|
{
|
|
av_packet_free(&packet);
|
|
avformat_free_context(format_context);
|
|
}
|
|
|
|
}
|