#include "common/common.h" #include "common/file.h" #include "common/logging/log.h" #include "kernel/fileSystem.h" #include "kernel/pthread.h" #include "libs/audio.h" #include "libs/libs.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include extern "C" { #include #include #include #include #include #include #include #include } namespace Libs::Audio::AvPlayer { LIB_NAME("AvPlayer", "AvPlayer"); using AvPlayerAllocate = KYTY_SYSV_ABI void* (*)(void*, uint32_t, uint32_t); using AvPlayerDeallocate = KYTY_SYSV_ABI void (*)(void*, void*); using AvPlayerAllocateTexture = KYTY_SYSV_ABI void* (*)(void*, uint32_t, uint32_t); using AvPlayerDeallocateTexture = KYTY_SYSV_ABI void (*)(void*, void*); using AvPlayerOpenFile = KYTY_SYSV_ABI int (*)(void*, const char*); using AvPlayerCloseFile = KYTY_SYSV_ABI int (*)(void*); using AvPlayerReadOffsetFile = KYTY_SYSV_ABI int (*)(void*, uint8_t*, uint64_t, uint32_t); using AvPlayerSizeFile = KYTY_SYSV_ABI uint64_t (*)(void*); using AvPlayerEventCallback = KYTY_SYSV_ABI void (*)(void*, int32_t, int32_t, void*); using Bool = uint8_t; constexpr int32_t AVPLAYER_EVENT_STATE_STOP = 0x01; constexpr int32_t AVPLAYER_EVENT_STATE_READY = 0x02; constexpr int32_t AVPLAYER_EVENT_STATE_PLAY = 0x03; constexpr int32_t AVPLAYER_EVENT_STATE_PAUSE = 0x04; constexpr int32_t AVPLAYER_EVENT_WARNING_ID = 0x20; constexpr int32_t AVPLAYER_ERROR_INVALID_PARAMS = -2140536831; constexpr int32_t AVPLAYER_ERROR_OPERATION_FAILED = -2140536830; constexpr int32_t AVPLAYER_ERROR_NO_MEMORY = -2140536829; constexpr int32_t AVPLAYER_ERROR_NOT_SUPPORTED = -2140536828; constexpr int32_t AVPLAYER_WARNING_LOOPING_BACK = -2140536671; constexpr int32_t AVPLAYER_WARNING_JUMP_COMPLETE = -2140536669; constexpr int32_t AVPLAYER_TRICK_SPEED_NORMAL = 100; constexpr int32_t AVPLAYER_TRICK_SPEED_MIN = 400; constexpr int32_t AVPLAYER_TRICK_SPEED_MAX = 3200; constexpr uint32_t AVPLAYER_DEMUX_BUFFER_DEFAULT = 4 * 1024 * 1024; constexpr uint32_t AVPLAYER_DEMUX_BUFFER_MIN = 128 * 1024; constexpr uint32_t AVPLAYER_DEMUX_BUFFER_MAX = 32 * 1024 * 1024; constexpr int AVPLAYER_AUDIO_BUFFERS = 8; enum AvPlayerUriType : uint32_t { AvPlayerUriTypeSource = 0 }; enum AvPlayerSourceType : uint32_t { AvPlayerSourceUnknown = 0, AvPlayerSourceFileMp4 = 1, AvPlayerSourceFileWebm = 2, AvPlayerSourceHls = 8 }; enum AvPlayerDebuglevels : int { AvplayerDbgNone, AvplayerDbgInfo, AvplayerDbgWarnings, AvplayerDbgAll }; enum AvPlayerStreamType : uint32_t { AvPlayerStreamUnknown = 0, AvPlayerStreamVideo = 1, AvPlayerStreamAudio = 2, AvPlayerStreamTimedText = 3 }; enum AvPlayerVideoDecoderType : uint32_t { AvPlayerVideoDecoderDefault = 0, AvPlayerVideoDecoderSoftware2 = 1 }; enum AvPlayerAudioDecoderType : uint32_t { AvPlayerAudioDecoderDefault = 0 }; enum AvPlayerAudioChannelOrder : uint32_t { AvPlayerAudioChannelOrderDefault = 0, AvPlayerAudioChannelOrderExt = 1, AvPlayerAudioChannelOrderLsRs = 2 }; struct AvPlayerMemAllocator { void* object_pointer = nullptr; AvPlayerAllocate allocate = nullptr; AvPlayerDeallocate deallocate = nullptr; AvPlayerAllocateTexture allocate_texture = nullptr; AvPlayerDeallocateTexture deallocate_texture = nullptr; }; struct AvPlayerFileReplacement { void* object_pointer = nullptr; AvPlayerOpenFile open = nullptr; AvPlayerCloseFile close = nullptr; AvPlayerReadOffsetFile read_offset = nullptr; AvPlayerSizeFile size = nullptr; }; struct AvPlayerEventReplacement { void* object_pointer = nullptr; AvPlayerEventCallback event_callback = nullptr; }; static void emit_event(AvPlayerEventReplacement event, int32_t id, void* data = nullptr) { if (event.event_callback != nullptr) { LOGF("\t event_id = %d\n", id); event.event_callback(event.object_pointer, id, 0, data); } } struct AvPlayerUri { const char* name = nullptr; uint32_t length = 0; }; struct AvPlayerSourceDetails { AvPlayerUri uri; uint8_t reserved1[64] = {}; AvPlayerSourceType source_type = AvPlayerSourceUnknown; uint8_t reserved2[44] = {}; }; struct AvPlayerInitData { AvPlayerMemAllocator memory_replacement; AvPlayerFileReplacement file_replacement; AvPlayerEventReplacement event_replacement; AvPlayerDebuglevels debug_level = AvplayerDbgNone; uint32_t base_priority = 0; int32_t num_output_video_framebuffers = 0; Bool auto_start = 0; uint8_t reserved[3] = {}; const char* default_language = nullptr; }; struct AvPlayerThreadInfo { uint32_t priority = 0; uint32_t stack_size = 0; uint64_t affinity = 0; uint8_t reserved[32] = {}; }; struct AvPlayerInitDataEx { size_t this_size = 0; AvPlayerMemAllocator memory_replacement; AvPlayerFileReplacement file_replacement; AvPlayerEventReplacement event_replacement; const char* default_language = nullptr; AvPlayerDebuglevels debug_level = AvplayerDbgNone; Bool auto_start = 0; uint8_t reserved[3] = {}; AvPlayerThreadInfo audio_decoder; AvPlayerThreadInfo video_decoder; AvPlayerThreadInfo demuxer; AvPlayerThreadInfo event; AvPlayerThreadInfo call_queue; AvPlayerThreadInfo http_command_processor; AvPlayerThreadInfo http_segment_manager; AvPlayerThreadInfo http_streamlist; AvPlayerThreadInfo file_streaming; int32_t num_output_video_framebuffers = 0; uint8_t reserved2[4] = {}; }; struct AvPlayerDecoderInit { union { AvPlayerVideoDecoderType video_type; AvPlayerAudioDecoderType audio_type; uint8_t reserved[4]; } decoder_type {}; uint8_t reserved[4] = {}; union { struct { uint64_t cpu_affinity_mask; int32_t cpu_thread_priority; uint8_t decode_input_queue_depth; uint8_t compute_pipe_id; uint8_t compute_queue_id; uint8_t reserved[17]; } video_sw2; struct { AvPlayerAudioChannelOrder audio_channel_order; uint8_t reserved[28]; } audio; uint8_t reserved[32]; } decoder_params {}; }; struct AvPlayerHttpContext { uint32_t http_context_id = 0; uint32_t ssl_context_id = 0; }; struct AvPlayerPostInitData { uint32_t demux_video_buffer_size = 0; uint8_t reserved[4] = {}; AvPlayerDecoderInit video_decoder_init; AvPlayerDecoderInit audio_decoder_init; AvPlayerHttpContext http_context; uint8_t reserved2[56] = {}; }; struct AvPlayerStartInfoEx { size_t this_size = 0; uint64_t start_time_milliseconds = 0; }; struct AvPlayerAudioEx { uint16_t channel_count = 0; uint8_t reserved[2] = {}; uint32_t sample_rate = 0; uint32_t size = 0; uint8_t language_code[4] = {}; uint8_t reserved1[64] = {}; }; struct AvPlayerAudio { uint16_t channel_count = 0; uint8_t reserved[2] = {}; uint32_t sample_rate = 0; uint32_t size = 0; uint8_t language_code[4] = {}; }; struct AvPlayerVideoEx { uint32_t width = 0; uint32_t height = 0; float aspect_ratio = 0.0f; uint8_t language_code[4] = {}; uint8_t reserved[4] = {}; uint32_t crop_left_offset = 0; uint32_t crop_right_offset = 0; uint32_t crop_top_offset = 0; uint32_t crop_bottom_offset = 0; uint32_t pitch = 0; uint8_t luma_bit_depth = 0; uint8_t chroma_bit_depth = 0; Bool video_full_range_flag = 0; uint8_t reserved2[5] = {}; double framerate = 0.0; uint32_t colour_primaries = 0; uint32_t transfer_characteristics = 0; uint8_t reserved3[16] = {}; }; struct AvPlayerVideo { uint32_t width = 0; uint32_t height = 0; float aspect_ratio = 0.0f; uint8_t language_code[4] = {}; }; struct AvPlayerTextPosition { int16_t top = 0; int16_t left = 0; int16_t bottom = 0; int16_t right = 0; }; struct AvPlayerTimedTextEx { uint8_t language_code[4] = {}; uint16_t text_size = 0; uint16_t font_size = 0; AvPlayerTextPosition position; uint8_t reserved1[64] = {}; }; struct AvPlayerTimedText { uint8_t language_code[4] = {}; uint16_t text_size = 0; uint16_t font_size = 0; AvPlayerTextPosition position; }; union AvPlayerStreamDetailsEx { AvPlayerAudioEx audio; AvPlayerVideoEx video; AvPlayerTimedTextEx subs; uint8_t reserved1[80]; }; union AvPlayerStreamDetails { AvPlayerAudio audio; AvPlayerVideo video; AvPlayerTimedText subs; uint8_t reserved[16]; }; struct AvPlayerFrameInfo { void* data = nullptr; uint8_t reserved[4] = {}; uint64_t time_stamp = 0; AvPlayerStreamDetails details {}; }; struct AvPlayerFrameInfoEx { void* data = nullptr; uint8_t reserved[4] = {}; uint64_t time_stamp = 0; AvPlayerStreamDetailsEx details {}; }; struct AvPlayerStreamInfo { AvPlayerStreamType type = AvPlayerStreamUnknown; uint8_t reserved[4] = {}; AvPlayerStreamDetails details {}; uint64_t duration = 0; }; struct AvPlayerStreamInfoEx { size_t this_size = 0; AvPlayerStreamType type = AvPlayerStreamUnknown; uint8_t reserved[4] = {}; AvPlayerStreamDetailsEx details {}; uint64_t duration = 0; }; static_assert(offsetof(AvPlayerPostInitData, video_decoder_init) == 8); static_assert(offsetof(AvPlayerVideoEx, framerate) == 48); static_assert(sizeof(AvPlayerStreamDetailsEx) == 80); static_assert(offsetof(AvPlayerStreamInfoEx, duration) == 96); static_assert(offsetof(AvPlayerFrameInfoEx, details) == 24); static std::string fferr(int e) { char buf[AV_ERROR_MAX_STRING_SIZE] = {}; av_make_error_string(buf, sizeof(buf), e); return buf; } static uint64_t to_ms(int64_t v, AVRational tb) { return v == AV_NOPTS_VALUE ? 0 : static_cast(std::max( 0, av_rescale_q(v, tb, AVRational {1, 1000}))); } static uint32_t align_up(uint32_t v, uint32_t a) { return a == 0 ? v : ((v + a - 1) / a) * a; } static bool ieq(std::string_view a, std::string_view b) { return a.size() == b.size() && std::equal(a.begin(), a.end(), b.begin(), [](char l, char r) { return std::tolower(static_cast(l)) == std::tolower(static_cast(r)); }); } static void lang(uint8_t out[4], AVDictionary* md) { std::memset(out, 0, 4); if (auto* e = av_dict_get(md, "language", nullptr, 0); e != nullptr && e->value != nullptr) { std::memcpy(out, e->value, std::min(3, std::strlen(e->value))); } else { out[0] = 'u'; out[1] = 'n'; out[2] = 'd'; } } static AvPlayerSourceType source_type_from_path(std::string_view path) { if (auto q = path.find_first_of("?#"); q != std::string_view::npos) { path = path.substr(0, q); } auto dot = path.rfind('.'); if (dot == std::string_view::npos) { return AvPlayerSourceUnknown; } auto ext = path.substr(dot); if (auto slash = ext.find('/'); slash != std::string_view::npos) { ext = ext.substr(0, slash); } if (ieq(ext, ".mp4") || ieq(ext, ".m4v") || ieq(ext, ".m4a") || ieq(ext, ".mov") || ieq(ext, ".m3d")) { return AvPlayerSourceFileMp4; } if (ieq(ext, ".webm")) { return AvPlayerSourceFileWebm; } if (ieq(ext, ".m3u8")) { return AvPlayerSourceHls; } return AvPlayerSourceUnknown; } static bool source_type_is_file(AvPlayerSourceType type) { return type == AvPlayerSourceUnknown || type == AvPlayerSourceFileMp4 || type == AvPlayerSourceFileWebm; } static bool codec_is_supported_for_source(AvPlayerSourceType source_type, AVMediaType media_type, AVCodecID codec_id) { if (source_type != AvPlayerSourceFileWebm) { return true; } switch (media_type) { case AVMEDIA_TYPE_VIDEO: return codec_id == AV_CODEC_ID_VP8 || codec_id == AV_CODEC_ID_VP9 || codec_id == AV_CODEC_ID_AV1; case AVMEDIA_TYPE_AUDIO: return codec_id == AV_CODEC_ID_OPUS || codec_id == AV_CODEC_ID_VORBIS; default: return true; } } static const AVCodec* find_decoder(AVCodecID codec_id) { const auto* decoder = avcodec_find_decoder(codec_id); if (decoder == nullptr && codec_id == AV_CODEC_ID_VP9) { Log::WriteToConsoleAndLog( "WARNING: AvPlayer: The game requested VP9 video, but FFmpeg has no VP9 " "decoder. Use FFmpeg with VP9 decoding enabled.\n"); } return decoder; } struct PacketDeleter { void operator()(AVPacket* packet) const { av_packet_free(&packet); } }; using Packet = std::unique_ptr; struct DemuxPacket { Packet packet; uint64_t timestamp_offset = 0; }; class PacketQueue { public: explicit PacketQueue(size_t byte_capacity): capacity(byte_capacity) {} bool Push(DemuxPacket value, const std::atomic_bool& stop) { const auto size = value.packet == nullptr ? 0u : static_cast(std::max(value.packet->size, 0)); std::unique_lock lock(mutex); available.wait(lock, [&] { return stop || bytes + size <= capacity || (queue.empty() && bytes == 0); }); if (stop) { return false; } bytes += size; queue.push_back(std::move(value)); available.notify_all(); return true; } std::optional WaitPop(const std::atomic_bool& stop, const std::atomic_bool& finished) { std::unique_lock lock(mutex); available.wait(lock, [&] { return stop || finished || !queue.empty(); }); if (stop || queue.empty()) { return std::nullopt; } auto value = std::move(queue.front()); queue.pop_front(); if (value.packet != nullptr) { bytes -= static_cast(std::max(value.packet->size, 0)); } available.notify_all(); return value; } void Clear() { std::lock_guard lock(mutex); queue.clear(); bytes = 0; available.notify_all(); } void Notify() { available.notify_all(); } private: std::mutex mutex; std::condition_variable available; std::deque queue; size_t bytes = 0; size_t capacity = 0; }; class GuestBuffer { public: GuestBuffer() = default; GuestBuffer(AvPlayerMemAllocator mem, uint32_t align, uint32_t size, bool texture) : m_mem(mem), m_size(size), m_texture(texture) { m_data = static_cast(texture ? mem.allocate_texture(mem.object_pointer, align, size) : mem.allocate(mem.object_pointer, align, size)); } ~GuestBuffer() { Reset(); } GuestBuffer(const GuestBuffer&) = delete; GuestBuffer& operator=(const GuestBuffer&) = delete; GuestBuffer(GuestBuffer&& r) noexcept { Move(r); } GuestBuffer& operator=(GuestBuffer&& r) noexcept { if (this != &r) { Reset(); Move(r); } return *this; } uint8_t* Get() const { return m_data; } bool Valid() const { return m_data != nullptr; } uint32_t Size() const { return m_size; } private: void Move(GuestBuffer& r) { m_mem = r.m_mem; m_data = r.m_data; m_size = r.m_size; m_texture = r.m_texture; r.m_data = nullptr; r.m_size = 0; } void Reset() { if (m_data != nullptr) { if (m_texture) { m_mem.deallocate_texture(m_mem.object_pointer, m_data); } else { m_mem.deallocate(m_mem.object_pointer, m_data); } m_data = nullptr; } } AvPlayerMemAllocator m_mem {}; uint8_t* m_data = nullptr; uint32_t m_size = 0; bool m_texture = false; }; template class WorkQueue { public: void Push(T value) { std::lock_guard lock(mutex); queue.push_back(std::move(value)); available.notify_all(); } std::optional TryPop() { std::lock_guard lock(mutex); if (queue.empty()) { return std::nullopt; } auto value = std::move(queue.front()); queue.pop_front(); available.notify_all(); return value; } template std::optional TryPopIf(Predicate&& predicate) { std::lock_guard lock(mutex); if (queue.empty() || !std::forward(predicate)(queue.front())) { return std::nullopt; } auto value = std::move(queue.front()); queue.pop_front(); available.notify_all(); return value; } std::optional WaitPop(const std::atomic_bool& stop) { std::unique_lock lock(mutex); available.wait(lock, [&] { return stop || !queue.empty(); }); if (stop || queue.empty()) { return std::nullopt; } auto value = std::move(queue.front()); queue.pop_front(); available.notify_all(); return value; } bool Empty() const { std::lock_guard lock(mutex); return queue.empty(); } void Clear() { std::lock_guard lock(mutex); queue.clear(); available.notify_all(); } void Notify() { available.notify_all(); } private: mutable std::mutex mutex; std::condition_variable available; std::deque queue; }; struct ReadyFrame { std::unique_ptr buffer; AvPlayerFrameInfoEx info {}; uint64_t timestamp_offset = 0; }; class FileStreamer { public: explicit FileStreamer(AvPlayerFileReplacement f): file(f) {} ~FileStreamer() { if (ctx != nullptr) { av_freep(&ctx->buffer); avio_context_free(&ctx); } if (opened) { file.close(file.object_pointer); } } bool Init(const std::string& path) { if (file.open != nullptr) { if (file.close == nullptr || file.read_offset == nullptr || file.size == nullptr || file.open(file.object_pointer, path.c_str()) < 0) { return false; } opened = true; size = file.size(file.object_pointer); } else { if (!local_file.Open(LibKernel::FileSystem::GetRealFilename(path), Common::File::Mode::Read)) { return false; } size = local_file.Size(); } if (size == 0 || size > static_cast(std::numeric_limits::max())) { return false; } constexpr int buffer_size = 64 * 1024; auto* buf = static_cast(av_malloc(buffer_size)); if (buf == nullptr) { return false; } ctx = avio_alloc_context(buf, buffer_size, 0, this, Read, nullptr, Seek); if (ctx == nullptr) { av_free(buf); } return ctx != nullptr; } AVIOContext* Context() const { return ctx; } private: static int Read(void* opaque, uint8_t* buf, int len) { auto* s = static_cast(opaque); if (s->pos >= s->size) { return AVERROR_EOF; } len = static_cast(std::min(len, s->size - s->pos)); int read = 0; if (s->opened) { read = s->file.read_offset(s->file.object_pointer, buf, s->pos, static_cast(len)); } else { uint32_t bytes = 0; s->local_file.Read(buf, static_cast(len), &bytes); read = static_cast(bytes); } if (read <= 0 || read > len) { return read < 0 ? read : AVERROR(EIO); } s->pos += static_cast(read); return read; } static int64_t Seek(void* opaque, int64_t offset, int whence) { auto* s = static_cast(opaque); if ((whence & AVSEEK_SIZE) != 0) { return static_cast(s->size); } whence &= ~AVSEEK_FORCE; uint64_t base = 0; switch (whence) { case SEEK_SET: break; case SEEK_CUR: base = s->pos; break; case SEEK_END: base = s->size; break; default: return AVERROR(EINVAL); } const auto distance = offset < 0 ? uint64_t {0} - static_cast(offset) : static_cast(offset); if ((offset < 0 && distance > base) || (offset >= 0 && distance > std::numeric_limits::max() - base)) { return AVERROR(EINVAL); } const auto position = offset < 0 ? base - distance : base + distance; if (!s->opened && !s->local_file.Seek(position)) { return AVERROR(EIO); } s->pos = position; return static_cast(position); } AvPlayerFileReplacement file; Common::File local_file; bool opened = false; uint64_t pos = 0; uint64_t size = 0; AVIOContext* ctx = nullptr; }; class Source { public: Source(AvPlayerMemAllocator m, AvPlayerFileReplacement f, AvPlayerEventReplacement e, int32_t video_buffers, bool vdec2, uint32_t demux_buffer_size) : mem(m), file(f), event(e), max_video_buffers(std::clamp(video_buffers <= 0 ? 2 : video_buffers, 2, 16)), use_vdec2(vdec2), video_packets( std::clamp(demux_buffer_size == 0 ? AVPLAYER_DEMUX_BUFFER_DEFAULT : demux_buffer_size, AVPLAYER_DEMUX_BUFFER_MIN, AVPLAYER_DEMUX_BUFFER_MAX)), audio_packets( std::clamp(demux_buffer_size == 0 ? AVPLAYER_DEMUX_BUFFER_DEFAULT : demux_buffer_size, AVPLAYER_DEMUX_BUFFER_MIN, AVPLAYER_DEMUX_BUFFER_MAX)) {} ~Source() { Close(); } int Init(const std::string& p, AvPlayerSourceType requested) { path = p; source_type = requested == AvPlayerSourceUnknown ? source_type_from_path(path) : requested; if (source_type == AvPlayerSourceHls) { return AVPLAYER_ERROR_NOT_SUPPORTED; } if (!source_type_is_file(source_type)) { return AVPLAYER_ERROR_INVALID_PARAMS; } auto* raw = avformat_alloc_context(); if (raw == nullptr) { return AVPLAYER_ERROR_NO_MEMORY; } raw->interrupt_callback.callback = [](void* opaque) { return static_cast(opaque)->interrupt_io.load() ? 1 : 0; }; raw->interrupt_callback.opaque = this; streamer = std::make_unique(file); if (!streamer->Init(path)) { avformat_free_context(raw); return AVPLAYER_ERROR_OPERATION_FAILED; } raw->pb = streamer->Context(); if (const auto rc = avformat_open_input(&raw, file.open == nullptr ? path.c_str() : nullptr, nullptr, nullptr); rc < 0) { LOGF("\t avformat_open_input failed: %s path=%s\n", fferr(rc).c_str(), path.c_str()); return AVPLAYER_ERROR_OPERATION_FAILED; } fmt = raw; if (auto rc = avformat_find_stream_info(fmt, nullptr); rc < 0) { LOGF("\t avformat_find_stream_info failed: %s\n", fferr(rc).c_str()); return AVPLAYER_ERROR_OPERATION_FAILED; } return 0; } int StreamCount() const { return fmt == nullptr ? 0 : static_cast(fmt->nb_streams); } int Enable(uint32_t id) { std::scoped_lock lifecycle_lock(lifecycle_mutex); std::lock_guard state_lock(mutex); if (state == State::Playing) { return AVPLAYER_ERROR_OPERATION_FAILED; } if (fmt == nullptr || id >= fmt->nb_streams) { return AVPLAYER_ERROR_OPERATION_FAILED; } if (!StreamSupported(static_cast(id))) { return AVPLAYER_ERROR_NOT_SUPPORTED; } switch (fmt->streams[id]->codecpar->codec_type) { case AVMEDIA_TYPE_VIDEO: video_id = static_cast(id); return 0; case AVMEDIA_TYPE_AUDIO: audio_id = static_cast(id); return 0; default: return AVPLAYER_ERROR_NOT_SUPPORTED; } } int Disable(uint32_t id) { std::scoped_lock lifecycle_lock(lifecycle_mutex); std::lock_guard state_lock(mutex); if (state == State::Playing) { return AVPLAYER_ERROR_OPERATION_FAILED; } if (video_id == static_cast(id)) { video_id.reset(); return 0; } if (audio_id == static_cast(id)) { audio_id.reset(); return 0; } return AVPLAYER_ERROR_OPERATION_FAILED; } int Change(uint32_t old_id, uint32_t new_id) { std::scoped_lock lifecycle_lock(lifecycle_mutex); if (fmt == nullptr || new_id >= fmt->nb_streams) { return AVPLAYER_ERROR_OPERATION_FAILED; } if (!StreamSupported(static_cast(new_id))) { return AVPLAYER_ERROR_NOT_SUPPORTED; } uint64_t time = 0; bool restart = false; bool was_paused = false; { std::lock_guard state_lock(mutex); if (audio_id != static_cast(old_id) || fmt->streams[new_id]->codecpar->codec_type != AVMEDIA_TYPE_AUDIO) { return AVPLAYER_ERROR_OPERATION_FAILED; } time = CurrentTimeNoLock(); restart = state == State::Playing; was_paused = paused; if (restart) { state = State::Ready; } } if (restart) { StopWorkers(); } { std::lock_guard state_lock(mutex); audio_id = static_cast(new_id); } if (!restart) { return 0; } return StartImpl(time, was_paused); } int Start(uint64_t ms = 0) { std::scoped_lock lifecycle_lock(lifecycle_mutex); return StartImpl(ms); } int StartImpl(uint64_t ms, bool paused_after_start = false, bool show_seek_frame = false) { { std::lock_guard lock(mutex); state = State::Ready; } StopWorkers(); interrupt_io = false; int result = 0; { std::lock_guard lock(mutex); ResetNoLock(true); if (!video_id && !audio_id) { AutoEnable(); } if (!video_id && !audio_id) { state = State::Stopped; return AVPLAYER_ERROR_OPERATION_FAILED; } if (!OpenCodecs()) { ResetNoLock(true); state = State::Stopped; return AVPLAYER_ERROR_OPERATION_FAILED; } if (!AllocateBuffers()) { ResetNoLock(true); state = State::Stopped; return AVPLAYER_ERROR_NO_MEMORY; } SeekNoLock(ms); start_time_ms = ms; clock_start = std::chrono::steady_clock::now(); paused_extra = {}; paused = paused_after_start; pause_time = clock_start; seek_video_frame_pending = paused_after_start && show_seek_frame && video_id.has_value(); } if (!StartWorkers()) { std::lock_guard lock(mutex); state = State::Stopped; ResetNoLock(true); result = AVPLAYER_ERROR_OPERATION_FAILED; } if (result == 0 && video_id) { ::printf("AvPlayer video started playing\n"); } return result; } int Stop() { bool was_playing_video = false; AvPlayerEventReplacement stop_event {}; { std::scoped_lock lifecycle_lock(lifecycle_mutex); { std::lock_guard lock(mutex); if (state == State::Playing) { was_playing_video = video_id.has_value(); stop_event = event; } state = State::Stopped; } StopWorkers(); { std::lock_guard lock(mutex); ResetNoLock(); } } if (was_playing_video) { ::printf("AvPlayer video stopped\n"); } emit_event(stop_event, AVPLAYER_EVENT_STATE_STOP); return 0; } int Pause() { std::scoped_lock lifecycle_lock(lifecycle_mutex); std::lock_guard lock(mutex); if (state != State::Playing) { return AVPLAYER_ERROR_OPERATION_FAILED; } paused = true; pause_time = std::chrono::steady_clock::now(); return 0; } void Resume() { std::scoped_lock lifecycle_lock(lifecycle_mutex); std::lock_guard lock(mutex); if (paused) { paused_extra += std::chrono::steady_clock::now() - pause_time; } paused = false; seek_video_frame_pending = false; } void SetLoop(bool v) { loop = v; } void SetSync(uint32_t v) { std::lock_guard lock(mutex); sync_mode = v; } void SetTrick(int32_t v) { std::lock_guard lock(mutex); trick_speed = v; } bool Active() const { std::lock_guard lock(mutex); return state == State::Ready || (state == State::Playing && !pipeline_failed && !DrainedNoLock()); } uint64_t CurrentTime() const { std::lock_guard lock(mutex); return CurrentTimeNoLock(); } int Jump(uint64_t ms) { std::scoped_lock lifecycle_lock(lifecycle_mutex); bool was_paused = false; { std::lock_guard lock(mutex); if (state != State::Playing) { return AVPLAYER_ERROR_OPERATION_FAILED; } was_paused = paused; } return StartImpl(ms, was_paused, true); } uint64_t CurrentTimeNoLock() const { if (state != State::Playing) { return 0; } using namespace std::chrono; auto now = paused ? pause_time : steady_clock::now(); return start_time_ms + static_cast( duration_cast(now - clock_start - paused_extra).count()); } int Info(uint32_t id, AvPlayerStreamInfo* out) const { if (out == nullptr) { return AVPLAYER_ERROR_INVALID_PARAMS; } std::memset(out, 0, sizeof(*out)); if (fmt == nullptr || id >= fmt->nb_streams) { return AVPLAYER_ERROR_OPERATION_FAILED; } FillCommon(id, &out->type, &out->duration, &out->details, nullptr); return 0; } int InfoEx(uint32_t id, AvPlayerStreamInfoEx* out) const { if (out == nullptr) { return AVPLAYER_ERROR_INVALID_PARAMS; } std::memset(out, 0, sizeof(*out)); out->this_size = sizeof(*out); if (fmt == nullptr || id >= fmt->nb_streams) { return AVPLAYER_ERROR_OPERATION_FAILED; } FillCommon(id, &out->type, &out->duration, nullptr, &out->details); return 0; } bool Video(AvPlayerFrameInfoEx* out) { if (out == nullptr) { return false; } std::lock_guard lock(mutex); if (state != State::Playing || !video_id) { return false; } const bool deliver_seek_frame = paused && seek_video_frame_pending; if (paused && !deliver_seek_frame) { return false; } auto frame = video_frames.TryPopIf([&](const ReadyFrame& candidate) { if (deliver_seek_frame || sync_mode != 0) { return true; } auto now = CurrentTimeNoLock(); return now == 0 || candidate.info.time_stamp + candidate.timestamp_offset <= now; }); if (!frame) { return false; } if (current_video) { video_buffers.Push(std::move(current_video->buffer)); } current_video = std::move(*frame); *out = current_video->info; RecordLoopBoundary(*current_video); if (deliver_seek_frame) { seek_video_frame_pending = false; } return true; } bool Audio(AvPlayerFrameInfo* out) { if (out == nullptr) { return false; } std::lock_guard lock(mutex); if (paused || state != State::Playing || !audio_id || trick_speed != AVPLAYER_TRICK_SPEED_NORMAL) { return false; } auto frame = audio_frames.TryPop(); if (!frame) { return false; } if (current_audio) { audio_buffers.Push(std::move(current_audio->buffer)); } current_audio = std::move(*frame); std::memset(out, 0, sizeof(*out)); out->data = current_audio->info.data; out->time_stamp = current_audio->info.time_stamp; out->details.audio.channel_count = current_audio->info.details.audio.channel_count; out->details.audio.sample_rate = current_audio->info.details.audio.sample_rate; out->details.audio.size = current_audio->info.details.audio.size; std::memcpy(out->details.audio.language_code, current_audio->info.details.audio.language_code, 4); start_time_ms = current_audio->info.time_stamp + current_audio->timestamp_offset; clock_start = std::chrono::steady_clock::now(); paused_extra = {}; RecordLoopBoundary(*current_audio); return true; } std::optional TakeWarning() { std::lock_guard lock(mutex); if (pending_loop_warnings == 0) { return std::nullopt; } --pending_loop_warnings; return AVPLAYER_WARNING_LOOPING_BACK; } private: enum class State { Ready, Playing, Stopped }; bool DrainedNoLock() const { return demux_eof && video_done && audio_done && video_frames.Empty() && audio_frames.Empty(); } void RecordLoopBoundary(const ReadyFrame& frame) { if (frame.timestamp_offset > last_output_loop_offset) { last_output_loop_offset = frame.timestamp_offset; ++pending_loop_warnings; } } void Close() { Stop(); if (fmt != nullptr) { avformat_close_input(&fmt); } streamer.reset(); } void ResetNoLock(bool retain_output_buffers = false) { video_packets.Clear(); audio_packets.Clear(); video_frames.Clear(); audio_frames.Clear(); video_buffers.Clear(); audio_buffers.Clear(); if (!retain_output_buffers) { current_video.reset(); current_audio.reset(); } if (sws != nullptr) { sws_freeContext(sws); sws = nullptr; } sws_src_width = 0; sws_src_height = 0; sws_src_format = AV_PIX_FMT_NONE; if (swr != nullptr) { swr_free(&swr); } if (video_ctx != nullptr) { avcodec_free_context(&video_ctx); } if (audio_ctx != nullptr) { avcodec_free_context(&audio_ctx); } demux_eof = true; video_done = true; audio_done = true; seek_video_frame_pending = false; last_output_loop_offset = 0; pending_loop_warnings = 0; } void AutoEnable() { for (uint32_t i = 0; i < fmt->nb_streams; i++) { if (!StreamSupported(static_cast(i))) { continue; } auto t = fmt->streams[i]->codecpar->codec_type; if (t == AVMEDIA_TYPE_VIDEO && !video_id) { video_id = static_cast(i); } if (t == AVMEDIA_TYPE_AUDIO && !audio_id) { audio_id = static_cast(i); } } } bool StreamSupported(int id) const { if (fmt == nullptr || id < 0 || id >= static_cast(fmt->nb_streams)) { return false; } auto* s = fmt->streams[id]; auto t = s->codecpar->codec_type; if (t != AVMEDIA_TYPE_VIDEO && t != AVMEDIA_TYPE_AUDIO) { return true; } if (!codec_is_supported_for_source(source_type, t, s->codecpar->codec_id)) { LOGF("\t unsupported codec for source type: stream=%d codec=%d source=%u\n", id, static_cast(s->codecpar->codec_id), static_cast(source_type)); return false; } if (find_decoder(s->codecpar->codec_id) == nullptr) { LOGF("\t decoder not found: stream=%d codec=%d\n", id, static_cast(s->codecpar->codec_id)); return false; } return true; } bool OpenCodec(int id, AVCodecContext** out) { auto* s = fmt->streams[id]; auto* dec = find_decoder(s->codecpar->codec_id); if (dec == nullptr) { LOGF("\t avcodec_find_decoder failed: stream=%d codec=%d\n", id, static_cast(s->codecpar->codec_id)); return false; } auto* c = avcodec_alloc_context3(dec); if (c == nullptr) { return false; } if (avcodec_parameters_to_context(c, s->codecpar) < 0 || avcodec_open2(c, dec, nullptr) < 0) { avcodec_free_context(&c); return false; } *out = c; return true; } bool OpenCodecs() { if (video_id && !OpenCodec(video_id.value(), &video_ctx)) { return false; } if (audio_id && !OpenCodec(audio_id.value(), &audio_ctx)) { return false; } return true; } bool AllocateBuffers() { if (video_id) { auto size = VideoBufferSize(fmt->streams[video_id.value()]); auto retained = current_video ? 1u : 0u; for (size_t i = retained; i < static_cast(max_video_buffers); i++) { auto buffer = std::make_unique(mem, 0x100, size, true); if (!buffer->Valid()) { return false; } video_buffers.Push(std::move(buffer)); } } if (audio_id) { auto channels = std::max(1, audio_ctx->ch_layout.nb_channels); auto samples = std::max(1024, audio_ctx->frame_size); auto size = static_cast(channels * samples * sizeof(int16_t)); auto retained = current_audio ? 1 : 0; for (int i = retained; i < AVPLAYER_AUDIO_BUFFERS; i++) { auto buffer = std::make_unique(mem, 0x100, size, false); if (!buffer->Valid()) { return false; } audio_buffers.Push(std::move(buffer)); } } return true; } void SeekNoLock(uint64_t ms) { int sid = video_id.value_or(audio_id.value_or(-1)); int64_t ts = sid >= 0 ? av_rescale_q(static_cast(ms), AVRational {1, 1000}, fmt->streams[sid]->time_base) : static_cast(ms) * 1000; avformat_seek_file(fmt, sid, INT64_MIN, ts, INT64_MAX, 0); if (video_ctx != nullptr) { avcodec_flush_buffers(video_ctx); } if (audio_ctx != nullptr) { avcodec_flush_buffers(audio_ctx); } video_packets.Clear(); audio_packets.Clear(); } static KYTY_SYSV_ABI void* DemuxEntry(void* arg) { static_cast(arg)->Demux(); return nullptr; } static KYTY_SYSV_ABI void* VideoDecoderEntry(void* arg) { static_cast(arg)->VideoDecoder(); return nullptr; } static KYTY_SYSV_ABI void* AudioDecoderEntry(void* arg) { static_cast(arg)->AudioDecoder(); return nullptr; } bool CreateWorker(LibKernel::Pthread& thread, LibKernel::pthread_entry_func_t entry, const char* name) { LibKernel::Pthread created = nullptr; if (LibKernel::PthreadCreate(&created, nullptr, entry, this, name) != 0) { return false; } thread = created; return true; } bool StartWorkers() { std::unique_lock lock(mutex); worker_stop = false; interrupt_io = false; pipeline_failed = false; demux_eof = false; video_done = !video_id; audio_done = !audio_id; if ((video_id && !CreateWorker(video_thread, VideoDecoderEntry, "AvPlayerVideoDecoder")) || (audio_id && !CreateWorker(audio_thread, AudioDecoderEntry, "AvPlayerAudioDecoder")) || !CreateWorker(demux_thread, DemuxEntry, "AvPlayerDemuxer")) { lock.unlock(); StopWorkers(); return false; } state = State::Playing; return true; } void NotifyWorkers() { video_packets.Notify(); audio_packets.Notify(); video_buffers.Notify(); audio_buffers.Notify(); } void StopWorkers() { { std::lock_guard lock(mutex); worker_stop = true; interrupt_io = true; } NotifyWorkers(); if (demux_thread != nullptr) { LibKernel::PthreadJoin(demux_thread, nullptr); demux_thread = nullptr; } if (video_thread != nullptr) { LibKernel::PthreadJoin(video_thread, nullptr); video_thread = nullptr; } if (audio_thread != nullptr) { LibKernel::PthreadJoin(audio_thread, nullptr); audio_thread = nullptr; } } void FailPipeline() { { std::lock_guard lock(mutex); pipeline_failed = true; worker_stop = true; interrupt_io = true; } NotifyWorkers(); } void Demux() { uint64_t video_offset = 0; uint64_t audio_offset = 0; uint64_t loop_period = fmt->duration > 0 ? static_cast(fmt->duration / 1000) : 0; if (loop_period == 0) { if (video_id) { loop_period = Duration(fmt->streams[video_id.value()]); } if (audio_id) { loop_period = std::max(loop_period, Duration(fmt->streams[audio_id.value()])); } } while (!worker_stop) { Packet packet(av_packet_alloc()); if (packet == nullptr) { FailPipeline(); break; } auto result = av_read_frame(fmt, packet.get()); if (result < 0) { if (result == AVERROR_EOF && loop) { video_offset += loop_period; audio_offset += loop_period; if ((video_id && !video_packets.Push({nullptr, video_offset}, worker_stop)) || (audio_id && !audio_packets.Push({nullptr, audio_offset}, worker_stop))) { break; } int stream = video_id.value_or(audio_id.value_or(-1)); if (avformat_seek_file(fmt, stream, INT64_MIN, 0, INT64_MAX, 0) < 0) { break; } continue; } if (result != AVERROR_EOF && !worker_stop) { LOGF("\t av_read_frame failed: %s\n", fferr(result).c_str()); FailPipeline(); } break; } if (video_id && packet->stream_index == video_id.value()) { if (!video_packets.Push({std::move(packet), video_offset}, worker_stop)) { break; } } else if (audio_id && packet->stream_index == audio_id.value()) { if (!audio_packets.Push({std::move(packet), audio_offset}, worker_stop)) { break; } } } demux_eof = true; video_packets.Notify(); audio_packets.Notify(); } void VideoDecoder() { Decoder(video_packets, video_ctx, video_buffers, video_frames, video_done, &Source::PrepareVideo, "video"); } void AudioDecoder() { Decoder(audio_packets, audio_ctx, audio_buffers, audio_frames, audio_done, &Source::PrepareAudio, "audio"); } using PrepareFrame = bool (Source::*)(AVFrame*, std::unique_ptr&, AvPlayerFrameInfoEx*); void Decoder(PacketQueue& packets, AVCodecContext* codec, WorkQueue>& buffers, WorkQueue& frames, std::atomic_bool& done, PrepareFrame prepare, const char* kind) { uint64_t timestamp_offset = 0; while (!worker_stop) { auto item = packets.WaitPop(worker_stop, demux_eof); if (!item) { if (demux_eof && !DecodePacket(codec, nullptr, timestamp_offset, buffers, frames, prepare, kind) && !worker_stop) { FailPipeline(); } break; } if (item->packet == nullptr) { if (!DecodePacket(codec, nullptr, timestamp_offset, buffers, frames, prepare, kind)) { if (!worker_stop) { FailPipeline(); } break; } avcodec_flush_buffers(codec); timestamp_offset = item->timestamp_offset; continue; } timestamp_offset = item->timestamp_offset; if (!DecodePacket(codec, item->packet.get(), timestamp_offset, buffers, frames, prepare, kind)) { if (!worker_stop) { FailPipeline(); } break; } } done = true; } bool DecodePacket(AVCodecContext* codec, AVPacket* packet, uint64_t timestamp_offset, WorkQueue>& buffers, WorkQueue& frames, PrepareFrame prepare, const char* kind) { auto result = avcodec_send_packet(codec, packet); if (result == AVERROR(EAGAIN)) { if (!ReceiveFrames(codec, timestamp_offset, buffers, frames, prepare, kind)) { return false; } result = avcodec_send_packet(codec, packet); } if (result == AVERROR_EOF) { return true; } if (result < 0) { LOGF("\t avcodec_send_packet %s failed: %s\n", kind, fferr(result).c_str()); return false; } return ReceiveFrames(codec, timestamp_offset, buffers, frames, prepare, kind); } bool ReceiveFrames(AVCodecContext* codec, uint64_t timestamp_offset, WorkQueue>& buffers, WorkQueue& frames, PrepareFrame prepare, const char* kind) { while (!worker_stop) { AVFrame* frame = av_frame_alloc(); if (frame == nullptr) { return false; } auto result = avcodec_receive_frame(codec, frame); if (result == AVERROR(EAGAIN) || result == AVERROR_EOF) { av_frame_free(&frame); return true; } if (result < 0) { LOGF("\t avcodec_receive_frame %s failed: %s\n", kind, fferr(result).c_str()); av_frame_free(&frame); return false; } auto buffer = buffers.WaitPop(worker_stop); if (!buffer) { av_frame_free(&frame); return false; } ReadyFrame ready {.buffer = std::move(*buffer)}; if (!(this->*prepare)(frame, ready.buffer, &ready.info)) { buffers.Push(std::move(ready.buffer)); av_frame_free(&frame); return false; } ready.timestamp_offset = timestamp_offset; frames.Push(std::move(ready)); av_frame_free(&frame); } return false; } uint32_t Width(AVStream* s) const { return use_vdec2 ? static_cast(s->codecpar->width) : align_up(static_cast(s->codecpar->width), 16); } uint32_t Height(AVStream* s) const { return use_vdec2 ? static_cast(s->codecpar->height) : align_up(static_cast(s->codecpar->height), 16); } float Aspect(AVRational ratio) const { return ratio.num > 0 && ratio.den > 0 ? static_cast(av_q2d(ratio)) : 0.0f; } uint64_t Duration(AVStream* s) const { return s->duration != AV_NOPTS_VALUE ? to_ms(s->duration, s->time_base) : (fmt->duration > 0 ? static_cast(fmt->duration / 1000) : 0); } uint32_t VideoPitch(AVStream* s) const { return align_up(Width(s), 256u); } uint32_t VideoBufferSize(AVStream* s) const { return VideoPitch(s) * Height(s) * 3 / 2; } void FillVideo(AVStream* s, AvPlayerVideo* v) const { std::memset(v, 0, sizeof(*v)); v->width = Width(s); v->height = Height(s); v->aspect_ratio = Aspect(s->sample_aspect_ratio); lang(v->language_code, s->metadata); } void FillVideoEx(AVStream* s, AvPlayerVideoEx* v, AVRational aspect) const { std::memset(v, 0, sizeof(*v)); v->width = Width(s); v->height = Height(s); v->aspect_ratio = Aspect(aspect); v->pitch = VideoPitch(s); v->luma_bit_depth = 8; v->chroma_bit_depth = 8; v->video_full_range_flag = s->codecpar->color_range == AVCOL_RANGE_JPEG ? 1 : 0; auto fr = s->avg_frame_rate.num != 0 ? s->avg_frame_rate : s->r_frame_rate; v->framerate = av_q2d(fr); v->colour_primaries = static_cast(s->codecpar->color_primaries); v->transfer_characteristics = static_cast(s->codecpar->color_trc); lang(v->language_code, s->metadata); } void FillAudio(AVStream* s, AvPlayerAudio* a, uint32_t size) const { std::memset(a, 0, sizeof(*a)); a->channel_count = static_cast(s->codecpar->ch_layout.nb_channels); a->sample_rate = static_cast(s->codecpar->sample_rate); a->size = size; lang(a->language_code, s->metadata); } void FillAudioEx(AVStream* s, AvPlayerAudioEx* a, uint32_t size) const { std::memset(a, 0, sizeof(*a)); a->channel_count = static_cast(s->codecpar->ch_layout.nb_channels); a->sample_rate = static_cast(s->codecpar->sample_rate); a->size = size; lang(a->language_code, s->metadata); } void FillCommon(uint32_t id, AvPlayerStreamType* type, uint64_t* dur, AvPlayerStreamDetails* d, AvPlayerStreamDetailsEx* ex) const { auto* s = fmt->streams[id]; *dur = Duration(s); switch (s->codecpar->codec_type) { case AVMEDIA_TYPE_VIDEO: *type = AvPlayerStreamVideo; if (d) { FillVideo(s, &d->video); } if (ex) { FillVideoEx(s, &ex->video, s->sample_aspect_ratio); } break; case AVMEDIA_TYPE_AUDIO: *type = AvPlayerStreamAudio; if (d) { FillAudio(s, &d->audio, 0); } if (ex) { FillAudioEx(s, &ex->audio, 0); } break; case AVMEDIA_TYPE_SUBTITLE: *type = AvPlayerStreamTimedText; break; default: *type = AvPlayerStreamUnknown; break; } } bool PrepareVideo(AVFrame* src, std::unique_ptr& buffer, AvPlayerFrameInfoEx* info) { auto* s = fmt->streams[video_id.value()]; uint32_t h = Height(s); uint32_t pitch = VideoPitch(s); uint64_t size = static_cast(pitch) * h * 3 / 2; if (buffer == nullptr || src->width <= 0 || src->height <= 0 || static_cast(src->width) > pitch || static_cast(src->height) > h || size > buffer->Size()) { return false; } AVFrame* nv12 = src; AVFrame* tmp = nullptr; if (src->format != AV_PIX_FMT_NV12) { tmp = av_frame_alloc(); if (tmp == nullptr) { return false; } tmp->format = AV_PIX_FMT_NV12; tmp->width = src->width; tmp->height = src->height; if (av_frame_get_buffer(tmp, 0) < 0) { av_frame_free(&tmp); return false; } if (sws == nullptr || sws_src_width != src->width || sws_src_height != src->height || sws_src_format != src->format) { if (sws != nullptr) { sws_freeContext(sws); } sws = sws_getContext( src->width, src->height, static_cast(src->format), src->width, src->height, AV_PIX_FMT_NV12, SWS_FAST_BILINEAR, nullptr, nullptr, nullptr); sws_src_width = src->width; sws_src_height = src->height; sws_src_format = src->format; } if (sws == nullptr) { av_frame_free(&tmp); return false; } if (sws_scale(sws, src->data, src->linesize, 0, src->height, tmp->data, tmp->linesize) != src->height) { av_frame_free(&tmp); return false; } nv12 = tmp; } if (nv12->data[0] == nullptr || nv12->data[1] == nullptr || nv12->linesize[0] < src->width || nv12->linesize[1] < src->width) { if (tmp != nullptr) { av_frame_free(&tmp); } return false; } auto* dst = buffer->Get(); auto* c = dst + pitch * h; // PPSA02433 samples the padded columns; zero chroma would turn them green. std::memset(dst, s->codecpar->color_range == AVCOL_RANGE_JPEG ? 0 : 16, static_cast(pitch) * h); std::memset(c, 128, static_cast(pitch) * h / 2); for (int y = 0; y < src->height; y++) { std::memcpy(dst + y * pitch, nv12->data[0] + y * nv12->linesize[0], src->width); } for (int y = 0; y < src->height / 2; y++) { std::memcpy(c + y * pitch, nv12->data[1] + y * nv12->linesize[1], src->width); } std::memset(info, 0, sizeof(*info)); info->data = dst; info->time_stamp = to_ms( src->best_effort_timestamp != AV_NOPTS_VALUE ? src->best_effort_timestamp : src->pts, s->time_base); FillVideoEx(s, &info->details.video, src->sample_aspect_ratio); info->details.video.crop_left_offset = static_cast(src->crop_left); info->details.video.crop_right_offset = static_cast(src->crop_right + (pitch - src->width)); info->details.video.crop_top_offset = static_cast(src->crop_top); info->details.video.crop_bottom_offset = static_cast(src->crop_bottom + (h - src->height)); info->details.video.pitch = pitch; if (tmp) { av_frame_free(&tmp); } return true; } bool PrepareAudio(AVFrame* src, std::unique_ptr& buffer, AvPlayerFrameInfoEx* info) { auto timestamp = to_ms( src->best_effort_timestamp != AV_NOPTS_VALUE ? src->best_effort_timestamp : src->pts, fmt->streams[audio_id.value()]->time_base); AVFrame* pcm = src; AVFrame* tmp = nullptr; if (src->format != AV_SAMPLE_FMT_S16) { tmp = av_frame_alloc(); if (tmp == nullptr) { return false; } tmp->format = AV_SAMPLE_FMT_S16; tmp->sample_rate = src->sample_rate; tmp->nb_samples = src->nb_samples; av_channel_layout_copy(&tmp->ch_layout, &src->ch_layout); if (av_frame_get_buffer(tmp, 0) < 0) { av_frame_free(&tmp); return false; } if (swr == nullptr) { SwrContext* ctx = nullptr; AVChannelLayout out; av_channel_layout_copy(&out, &src->ch_layout); swr_alloc_set_opts2(&ctx, &out, AV_SAMPLE_FMT_S16, src->sample_rate, &src->ch_layout, static_cast(src->format), src->sample_rate, 0, nullptr); swr = ctx; swr_init(swr); av_channel_layout_uninit(&out); } if (swr == nullptr || swr_convert_frame(swr, tmp, src) < 0) { av_frame_free(&tmp); return false; } pcm = tmp; } auto channels = std::max(1, pcm->ch_layout.nb_channels); auto size = static_cast(channels * pcm->nb_samples * sizeof(int16_t)); if (size > buffer->Size()) { auto replacement = std::make_unique(mem, 0x100, size, false); if (!replacement->Valid()) { if (tmp) { av_frame_free(&tmp); } return false; } buffer = std::move(replacement); } std::memcpy(buffer->Get(), pcm->data[0], size); auto* s = fmt->streams[audio_id.value()]; std::memset(info, 0, sizeof(*info)); info->data = buffer->Get(); info->time_stamp = timestamp; FillAudioEx(s, &info->details.audio, size); info->details.audio.channel_count = static_cast(channels); info->details.audio.sample_rate = static_cast(pcm->sample_rate); if (tmp) { av_frame_free(&tmp); } return true; } AvPlayerMemAllocator mem; AvPlayerFileReplacement file; AvPlayerEventReplacement event; int max_video_buffers = 2; bool use_vdec2 = false; AvPlayerSourceType source_type = AvPlayerSourceUnknown; std::string path; std::unique_ptr streamer; AVFormatContext* fmt = nullptr; AVCodecContext* video_ctx = nullptr; AVCodecContext* audio_ctx = nullptr; SwsContext* sws = nullptr; SwrContext* swr = nullptr; int sws_src_width = 0; int sws_src_height = 0; int sws_src_format = AV_PIX_FMT_NONE; std::optional video_id; std::optional audio_id; PacketQueue video_packets; PacketQueue audio_packets; WorkQueue> video_buffers; WorkQueue> audio_buffers; WorkQueue video_frames; WorkQueue audio_frames; std::optional current_video; std::optional current_audio; LibKernel::Pthread demux_thread = nullptr; LibKernel::Pthread video_thread = nullptr; LibKernel::Pthread audio_thread = nullptr; std::atomic_bool worker_stop {true}; std::atomic_bool interrupt_io {false}; std::atomic_bool pipeline_failed {false}; std::atomic_bool demux_eof {true}; std::atomic_bool video_done {true}; std::atomic_bool audio_done {true}; std::mutex lifecycle_mutex; mutable std::mutex mutex; State state = State::Ready; bool paused = false; bool seek_video_frame_pending = false; std::atomic_bool loop {false}; int32_t trick_speed = AVPLAYER_TRICK_SPEED_NORMAL; uint32_t sync_mode = 0; uint64_t start_time_ms = 0; uint64_t last_output_loop_offset = 0; uint32_t pending_loop_warnings = 0; std::chrono::steady_clock::time_point clock_start {}; std::chrono::steady_clock::time_point pause_time {}; std::chrono::steady_clock::duration paused_extra {}; }; struct AvPlayerInternal { AvPlayerMemAllocator mem; AvPlayerFileReplacement file; AvPlayerEventReplacement event; AvPlayerPostInitData post_init {}; bool auto_start = false; int32_t video_buffers = 2; uint32_t sync_mode = 0; uint32_t start_bandwidth = 0; uint32_t minimum_bandwidth = 0; uint32_t maximum_bandwidth = 0; std::unique_ptr source; }; static bool valid_allocators(const AvPlayerMemAllocator& m) { return m.allocate && m.deallocate && m.allocate_texture && m.deallocate_texture; } static void pump_warnings(AvPlayerInternal* h) { if (h == nullptr || h->source == nullptr) { return; } while (auto w = h->source->TakeWarning()) { int32_t warning = *w; emit_event(h->event, AVPLAYER_EVENT_WARNING_ID, &warning); } } static AvPlayerInternal* create_player(const AvPlayerMemAllocator& mem, const AvPlayerFileReplacement& file, const AvPlayerEventReplacement& event, bool auto_start, int32_t video_buffers) { if (!valid_allocators(mem)) { return nullptr; } auto* h = new AvPlayerInternal; h->mem = mem; h->file = file; h->event = event; h->auto_start = auto_start || event.event_callback == nullptr; h->video_buffers = std::clamp(video_buffers <= 0 ? 2 : video_buffers, 2, 16); h->post_init.demux_video_buffer_size = 4 * 1024 * 1024; return h; } static int add_source(AvPlayerInternal* h, const std::string& filename, AvPlayerSourceType type) { if (h == nullptr) { return AVPLAYER_ERROR_INVALID_PARAMS; } if (h->source != nullptr) { return AVPLAYER_ERROR_OPERATION_FAILED; } bool vdec2 = h->post_init.video_decoder_init.decoder_type.video_type == AvPlayerVideoDecoderSoftware2; auto s = std::make_unique(h->mem, h->file, h->event, h->video_buffers, vdec2, h->post_init.demux_video_buffer_size); if (auto rc = s->Init(filename, type); rc < 0) { return rc; } s->SetSync(h->sync_mode); h->source = std::move(s); emit_event(h->event, AVPLAYER_EVENT_STATE_READY); if (h->auto_start) { auto rc = h->source->Start(); if (rc == 0) { emit_event(h->event, AVPLAYER_EVENT_STATE_PLAY); } return rc; } return 0; } static void log_init_common(const AvPlayerMemAllocator& mem, const AvPlayerFileReplacement& file, const AvPlayerEventReplacement& event, AvPlayerDebuglevels debug_level, int32_t buffers, uint32_t priority, Bool auto_start, const char* language) { LOGF("\t memory_replacement.object_pointer = %016" PRIx64 "\n", reinterpret_cast(mem.object_pointer)); LOGF("\t memory_replacement.allocate = %016" PRIx64 "\n\t memory_replacement.deallocate = %016" PRIx64 "\n", reinterpret_cast(mem.allocate), reinterpret_cast(mem.deallocate)); LOGF("\t memory_replacement.allocate_texture = %016" PRIx64 "\n", reinterpret_cast(mem.allocate_texture)); LOGF("\t memory_replacement.deallocate_texture = %016" PRIx64 "\n", reinterpret_cast(mem.deallocate_texture)); LOGF("\t file_replacement.object_pointer = %016" PRIx64 "\n", reinterpret_cast(file.object_pointer)); LOGF("\t file_replacement.open = %016" PRIx64 "\n\t file_replacement.close = %016" PRIx64 "\n\t file_replacement.read_offset = %016" PRIx64 "\n\t file_replacement.size = %016" PRIx64 "\n", reinterpret_cast(file.open), reinterpret_cast(file.close), reinterpret_cast(file.read_offset), reinterpret_cast(file.size)); LOGF("\t event_replacement.object_pointer = %016" PRIx64 "\n", reinterpret_cast(event.object_pointer)); LOGF("\t event_replacement.event_callback = %016" PRIx64 "\n", reinterpret_cast(event.event_callback)); LOGF("\t debug_level = %s\n\t num_output_video_framebuffers " "= %d\n\t base_priority " " = %u\n\t auto_start = %u\n\t default_language " " = %s\n", magic_enum::enum_name(debug_level), buffers, priority, auto_start, language == nullptr ? "(null)" : language); } AvPlayerInternal* KYTY_SYSV_ABI AvPlayerInit(AvPlayerInitData* init) { PRINT_NAME(); if (init == nullptr) { return nullptr; } log_init_common(init->memory_replacement, init->file_replacement, init->event_replacement, init->debug_level, init->num_output_video_framebuffers, init->base_priority, init->auto_start, init->default_language); return create_player(init->memory_replacement, init->file_replacement, init->event_replacement, init->auto_start != 0, init->num_output_video_framebuffers); } int KYTY_SYSV_ABI AvPlayerInitEx(const void* init_ex, AvPlayerInternal** handle) { PRINT_NAME(); if (init_ex == nullptr || handle == nullptr) { return AVPLAYER_ERROR_INVALID_PARAMS; } const auto* init = static_cast(init_ex); LOGF("\t this_size = %zu\n", init->this_size); log_init_common(init->memory_replacement, init->file_replacement, init->event_replacement, init->debug_level, init->num_output_video_framebuffers, 0, init->auto_start, init->default_language); *handle = create_player(init->memory_replacement, init->file_replacement, init->event_replacement, init->auto_start != 0, init->num_output_video_framebuffers); return *handle == nullptr ? AVPLAYER_ERROR_INVALID_PARAMS : 0; } int KYTY_SYSV_ABI AvPlayerPostInit(AvPlayerInternal* h, const void* post_init) { PRINT_NAME(); if (h == nullptr) { return AVPLAYER_ERROR_INVALID_PARAMS; } if (post_init != nullptr) { h->post_init = *static_cast(post_init); LOGF("\t demux_video_buffer_size = %u\n", h->post_init.demux_video_buffer_size); } return 0; } int KYTY_SYSV_ABI AvPlayerAddSource(AvPlayerInternal* h, const char* filename) { PRINT_NAME(); if (h == nullptr || filename == nullptr) { return AVPLAYER_ERROR_INVALID_PARAMS; } LOGF("\t filename = %s\n", filename); return add_source(h, filename, AvPlayerSourceUnknown); } int KYTY_SYSV_ABI AvPlayerAddSourceEx(AvPlayerInternal* h, uint32_t uri_type, const void* source_details) { PRINT_NAME(); if (h == nullptr || uri_type != AvPlayerUriTypeSource || source_details == nullptr) { return AVPLAYER_ERROR_INVALID_PARAMS; } const auto* d = static_cast(source_details); if (d->uri.name == nullptr || d->uri.length == 0) { return AVPLAYER_ERROR_INVALID_PARAMS; } if (d->source_type != AvPlayerSourceUnknown && d->source_type != AvPlayerSourceFileMp4 && d->source_type != AvPlayerSourceFileWebm && d->source_type != AvPlayerSourceHls) { return AVPLAYER_ERROR_INVALID_PARAMS; } std::string filename(d->uri.name, d->uri.length); LOGF("\t uri_type = %u\n\t source_type = %u\n\t filename = %s\n", uri_type, static_cast(d->source_type), filename.c_str()); return add_source(h, filename, d->source_type); } int KYTY_SYSV_ABI AvPlayerStreamCount(AvPlayerInternal* h) { PRINT_NAME(); return h == nullptr ? AVPLAYER_ERROR_INVALID_PARAMS : (h->source == nullptr ? 0 : h->source->StreamCount()); } int KYTY_SYSV_ABI AvPlayerGetStreamInfo(AvPlayerInternal* h, uint32_t stream_id, void* info) { PRINT_NAME(); if (h == nullptr || info == nullptr) { return AVPLAYER_ERROR_INVALID_PARAMS; } return h->source == nullptr ? AVPLAYER_ERROR_OPERATION_FAILED : h->source->Info(stream_id, static_cast(info)); } int KYTY_SYSV_ABI AvPlayerGetStreamInfoEx(AvPlayerInternal* h, uint32_t stream_id, void* info) { PRINT_NAME(); if (h == nullptr || info == nullptr) { return AVPLAYER_ERROR_INVALID_PARAMS; } return h->source == nullptr ? AVPLAYER_ERROR_OPERATION_FAILED : h->source->InfoEx(stream_id, static_cast(info)); } int KYTY_SYSV_ABI AvPlayerEnableStream(AvPlayerInternal* h, uint32_t stream_id) { PRINT_NAME(); return h == nullptr || h->source == nullptr ? AVPLAYER_ERROR_INVALID_PARAMS : h->source->Enable(stream_id); } int KYTY_SYSV_ABI AvPlayerDisableStream(AvPlayerInternal* h, uint32_t stream_id) { PRINT_NAME(); return h == nullptr || h->source == nullptr ? AVPLAYER_ERROR_INVALID_PARAMS : h->source->Disable(stream_id); } int KYTY_SYSV_ABI AvPlayerChangeStream(AvPlayerInternal* h, uint32_t old_stream_id, uint32_t new_stream_id) { PRINT_NAME(); return h == nullptr || h->source == nullptr ? AVPLAYER_ERROR_INVALID_PARAMS : h->source->Change(old_stream_id, new_stream_id); } int KYTY_SYSV_ABI AvPlayerStart(AvPlayerInternal* h) { PRINT_NAME(); if (h == nullptr || h->source == nullptr) { return AVPLAYER_ERROR_INVALID_PARAMS; } auto rc = h->source->Start(); if (rc == 0) { emit_event(h->event, AVPLAYER_EVENT_STATE_PLAY); } return rc; } int KYTY_SYSV_ABI AvPlayerStartEx(AvPlayerInternal* h, const void* start_info_ex) { PRINT_NAME(); if (h == nullptr || h->source == nullptr) { return AVPLAYER_ERROR_INVALID_PARAMS; } uint64_t ms = start_info_ex == nullptr ? 0 : static_cast(start_info_ex)->start_time_milliseconds; auto rc = h->source->Start(ms); if (rc == 0) { emit_event(h->event, AVPLAYER_EVENT_STATE_PLAY); } return rc; } int KYTY_SYSV_ABI AvPlayerStop(AvPlayerInternal* h) { PRINT_NAME(); if (h == nullptr || h->source == nullptr) { return AVPLAYER_ERROR_INVALID_PARAMS; } return h->source->Stop(); } int KYTY_SYSV_ABI AvPlayerPause(AvPlayerInternal* h) { PRINT_NAME(); if (h == nullptr || h->source == nullptr) { return AVPLAYER_ERROR_INVALID_PARAMS; } auto rc = h->source->Pause(); if (rc == 0) { emit_event(h->event, AVPLAYER_EVENT_STATE_PAUSE); } return rc; } int KYTY_SYSV_ABI AvPlayerResume(AvPlayerInternal* h) { PRINT_NAME(); if (h == nullptr || h->source == nullptr) { return AVPLAYER_ERROR_INVALID_PARAMS; } h->source->Resume(); emit_event(h->event, AVPLAYER_EVENT_STATE_PLAY); return 0; } int KYTY_SYSV_ABI AvPlayerSetLooping(AvPlayerInternal* h, Bool loop) { PRINT_NAME(); if (h == nullptr || h->source == nullptr) { return AVPLAYER_ERROR_INVALID_PARAMS; } h->source->SetLoop(loop != 0); return 0; } int KYTY_SYSV_ABI AvPlayerSetTrickSpeed(AvPlayerInternal* h, int32_t trick_speed) { PRINT_NAME(); if (h == nullptr || h->source == nullptr) { return AVPLAYER_ERROR_INVALID_PARAMS; } trick_speed = std::clamp(trick_speed, -AVPLAYER_TRICK_SPEED_MAX, AVPLAYER_TRICK_SPEED_MAX); if (trick_speed != AVPLAYER_TRICK_SPEED_NORMAL && trick_speed > -AVPLAYER_TRICK_SPEED_MIN && trick_speed < AVPLAYER_TRICK_SPEED_MIN) { return AVPLAYER_ERROR_NOT_SUPPORTED; } h->source->SetTrick(trick_speed); return trick_speed == AVPLAYER_TRICK_SPEED_NORMAL ? 0 : AVPLAYER_ERROR_NOT_SUPPORTED; } int KYTY_SYSV_ABI AvPlayerSetAvSyncMode(AvPlayerInternal* h, uint32_t sync_mode) { PRINT_NAME(); if (h == nullptr) { return AVPLAYER_ERROR_INVALID_PARAMS; } if (sync_mode > 1) { return AVPLAYER_ERROR_NOT_SUPPORTED; } h->sync_mode = sync_mode; if (h->source != nullptr) { h->source->SetSync(sync_mode); } return 0; } int KYTY_SYSV_ABI AvPlayerSetAvailableBandwidth(AvPlayerInternal* h, uint32_t start_bandwidth, uint32_t minimum_bandwidth, uint32_t maximum_bandwidth) { PRINT_NAME(); if (h == nullptr || (minimum_bandwidth != 0 && maximum_bandwidth != 0 && minimum_bandwidth > maximum_bandwidth)) { return AVPLAYER_ERROR_INVALID_PARAMS; } h->start_bandwidth = start_bandwidth; h->minimum_bandwidth = minimum_bandwidth; h->maximum_bandwidth = maximum_bandwidth; return h->source == nullptr ? 0 : AVPLAYER_ERROR_OPERATION_FAILED; } Bool KYTY_SYSV_ABI AvPlayerGetVideoData(AvPlayerInternal* h, AvPlayerFrameInfo* video_info) { PRINT_NAME(); if (h == nullptr || h->source == nullptr || video_info == nullptr) { return 0; } AvPlayerFrameInfoEx ex {}; if (!h->source->Video(&ex)) { pump_warnings(h); return 0; } std::memset(video_info, 0, sizeof(*video_info)); video_info->data = ex.data; video_info->time_stamp = ex.time_stamp; video_info->details.video.width = ex.details.video.width; video_info->details.video.height = ex.details.video.height; video_info->details.video.aspect_ratio = ex.details.video.aspect_ratio; std::memcpy(video_info->details.video.language_code, ex.details.video.language_code, 4); pump_warnings(h); return 1; } Bool KYTY_SYSV_ABI AvPlayerGetVideoDataEx(AvPlayerInternal* h, AvPlayerFrameInfoEx* video_info) { PRINT_NAME(); if (h == nullptr || h->source == nullptr || video_info == nullptr) { return 0; } auto ok = h->source->Video(video_info) ? 1 : 0; pump_warnings(h); return ok; } Bool KYTY_SYSV_ABI AvPlayerGetAudioData(AvPlayerInternal* h, AvPlayerFrameInfo* audio_info) { PRINT_NAME(); if (h == nullptr || h->source == nullptr || audio_info == nullptr) { return 0; } auto ok = h->source->Audio(audio_info) ? 1 : 0; pump_warnings(h); return ok; } Bool KYTY_SYSV_ABI AvPlayerIsActive(AvPlayerInternal* h) { PRINT_NAME(); return h != nullptr && (h->source == nullptr || h->source->Active()); } uint64_t KYTY_SYSV_ABI AvPlayerCurrentTime(AvPlayerInternal* h) { PRINT_NAME(); return h == nullptr || h->source == nullptr ? 0 : h->source->CurrentTime(); } int KYTY_SYSV_ABI AvPlayerJumpToTime(AvPlayerInternal* h, uint64_t time_ms) { PRINT_NAME(); if (h == nullptr || h->source == nullptr) { return AVPLAYER_ERROR_INVALID_PARAMS; } auto rc = h->source->Jump(time_ms); if (rc == 0) { int32_t w = AVPLAYER_WARNING_JUMP_COMPLETE; emit_event(h->event, AVPLAYER_EVENT_WARNING_ID, &w); } return rc; } int KYTY_SYSV_ABI AvPlayerClose(AvPlayerInternal* h) { PRINT_NAME(); if (h == nullptr) { return AVPLAYER_ERROR_INVALID_PARAMS; } h->source.reset(); delete h; return 0; } int KYTY_SYSV_ABI AvPlayerSetLogCallback(void* callback, void* user_data) { PRINT_NAME(); LOGF("\t callback = 0x%016" PRIx64 "\n\t user_data = 0x%016" PRIx64 "\n", reinterpret_cast(callback), reinterpret_cast(user_data)); return 0; } } // namespace Libs::Audio::AvPlayer