Files
AnyPS5/core/libs/prx/libSceAvPlayer/src/Player.cpp
T
Dean Galvin b62d02c2af feat(avplayer): decode H.264 and AAC with FFmpeg
Replace the decoder-less AvPlayer with a player that demuxes MP4 sources
with FFmpeg (opened by path or through the title's file replacement),
decodes H.264 and AAC on their own threads, and hands out NV12 frames and
interleaved 16-bit PCM in buffers from the title's allocators, presented
on a media clock that audio drives when it is enabled.

Both libSceAvPlayer and libSceAvPlayer.native are built from the same
sources and link the shared ffmpeg target. The struct size checks stay,
next to the types in SceTypes.hpp. ChangeStream and GetStreamInfoEx are
implemented; Init without allocators, StartEx, SetAvailableBandwidth,
SetLogCallback and negative trick speeds succeed as before and are listed
in TechnicalDebt. HLS sources throw.

guest_avplayer plays an embedded 6 KB MP4 (H.264 plus two AAC tracks).
2026-10-03 08:10:55 +13:00

365 lines
12 KiB
C++

// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <algorithm>
#include <chrono>
#include <cctype>
#include <cstring>
#include "prx/libSceAvPlayer/include/AvPlayer.hpp"
#include "prx/libc/include/General.hpp"
#include "prx/libc/include/GuestHeap.hpp"
namespace AvPlayer {
namespace {
bool EqualsIgnoreCase(std::string_view left, std::string_view right) {
return std::ranges::equal(left, right, [](unsigned char a, unsigned char b) { return std::tolower(a) == std::tolower(b); });
}
bool SameLanguage(const char* preferred, const char* code) {
return preferred[0] != '\0' && std::strncmp(preferred, code, 4) == 0;
}
void* APS5_VABI GuestAllocate_nid_no_patch(void*, std::uint32_t alignment, std::uint32_t size) {
return GuestHeap::GuestHeapAlign_nid_postfix(std::max<std::uint32_t>(alignment, 16), size);
}
void APS5_VABI GuestDeallocate_nid_no_patch(void*, void* memory) {
GuestHeap::GuestHeapFree_nid_postfix(memory);
}
AvPlayerMemAllocator WithGuestHeapFallback(AvPlayerMemAllocator memory) {
if (!memory.allocate || !memory.deallocate) {
memory.allocate = GuestAllocate_nid_no_patch;
memory.deallocate = GuestDeallocate_nid_no_patch;
}
if (!memory.allocate_texture || !memory.deallocate_texture) {
memory.allocate_texture = GuestAllocate_nid_no_patch;
memory.deallocate_texture = GuestDeallocate_nid_no_patch;
}
return memory;
}
}
std::uint32_t DetectSourceType(std::string_view path) {
std::string_view name = path;
if (name.find("://") != std::string_view::npos) name = name.substr(0, name.find_first_of("?#"));
const auto dot = name.rfind('.');
if (dot == std::string_view::npos) return SourceTypeUnknown;
auto extension = name.substr(dot);
extension = extension.substr(0, extension.find('/'));
for (const auto mp4 : {".mp4", ".m4v", ".m3d", ".m4a", ".mov"}) {
if (EqualsIgnoreCase(extension, mp4)) return SourceTypeFileMp4;
}
if (EqualsIgnoreCase(extension, ".m3u8")) return SourceTypeHls;
return SourceTypeUnknown;
}
Player::Player(const AvPlayerInitData& init, std::uint32_t bufferCount)
: memory(WithGuestHeapFallback(init.memory_replacement)), callback(init.event_replacement), videoBuffers(bufferCount) {
if (init.file_replacement.open && init.file_replacement.close && init.file_replacement.read_offset && init.file_replacement.size) {
file = init.file_replacement;
}
autoStartEnabled = init.auto_start || callback.event_callback == nullptr;
if (init.default_language) {
for (std::size_t index = 0; index < 3 && init.default_language[index] != '\0'; ++index) language[index] = init.default_language[index];
}
controller = std::thread([this] { controllerLoop(); });
}
Player::~Player() {
{
std::lock_guard lock(eventMutex);
quit = true;
}
eventCondition.notify_all();
controller.join();
std::lock_guard lock(mutex);
source.reset();
}
void Player::queue(const Event& event) {
{
std::lock_guard lock(eventMutex);
events.push_back(event);
}
eventCondition.notify_all();
}
void Player::OnWarning(std::int32_t code) {
queue({EventWarningId, code, false, false});
}
void Player::OnError() {
queue({0, 0, false, true});
}
void Player::controllerLoop() {
for (;;) {
Event event{};
bool pending = false;
{
std::unique_lock lock(eventMutex);
eventCondition.wait_for(lock, std::chrono::milliseconds(5), [this] { return quit || !events.empty(); });
if (quit) return;
if (!events.empty()) {
event = events.front();
events.pop_front();
pending = true;
}
}
if (pending) {
deliver(event);
} else {
checkEndOfFile();
}
}
}
void Player::deliver(const Event& event) {
if (event.error) {
std::lock_guard lock(mutex);
if (source && state != State::Stop) {
previous = state;
state = State::Error;
}
return;
}
if (event.autoStart) {
autoStart();
return;
}
if (!callback.event_callback) return;
std::int32_t warning = event.warning;
callback.event_callback(callback.object_ptr, event.id, 0, event.id == EventWarningId ? &warning : nullptr);
}
void Player::autoStart() {
const int count = StreamCount();
if (count <= 0) {
Stop();
return;
}
int video = -1;
int audio = -1;
for (int index = 0; index < count; ++index) {
AvPlayerStreamInfo info{};
if (GetStreamInfo(static_cast<std::uint32_t>(index), info) != SCE_OK) {
Stop();
return;
}
if (info.type == StreamTypeVideo && (video == -1 || SameLanguage(language, info.details.video.language_code))) video = index;
if (info.type == StreamTypeAudio && (audio == -1 || SameLanguage(language, info.details.audio.language_code))) audio = index;
}
if (video != -1) EnableStream(static_cast<std::uint32_t>(video));
if (audio != -1) EnableStream(static_cast<std::uint32_t>(audio));
Start();
}
void Player::checkEndOfFile() {
{
std::lock_guard lock(mutex);
if (!source || state != State::Play || !source->Finished()) return;
previous = state;
state = State::EndOfFile;
}
deliver({EventStateStop, 0, false, false});
}
int Player::PostInit(const AvPlayerPostInitData& data) {
std::lock_guard lock(mutex);
demuxVideoBytes = data.demux_video_buffer_size;
if (source) source->SetDemuxVideoBufferSize(demuxVideoBytes);
return SCE_OK;
}
int Player::AddSource(std::string_view path, std::uint32_t sourceType) {
if (path.empty()) return SCE_AVPLAYER_ERROR_OPERATION_FAILED;
if (sourceType == SourceTypeUnknown) sourceType = DetectSourceType(path);
if (sourceType == SourceTypeHls) NotImplemented_nid_no_patch("sceAvPlayerAddSource (HLS)");
std::lock_guard lock(mutex);
if (source) return SCE_AVPLAYER_ERROR_OPERATION_FAILED;
auto opened = OpenSource({memory, file, videoBuffers}, std::string(path), *this);
if (!opened) {
previous = state;
state = State::Error;
return SCE_AVPLAYER_ERROR_OPERATION_FAILED;
}
if (opened->StreamCount() == 0) {
source = std::move(opened);
previous = state;
state = State::Error;
queue({EventWarningId, SCE_AVPLAYER_ERROR_NOT_SUPPORTED, false, false});
return SCE_OK;
}
opened->SetLooping(looping);
opened->SetSpeed(speed);
opened->SetSyncMode(syncMode);
opened->SetDemuxVideoBufferSize(demuxVideoBytes);
source = std::move(opened);
previous = state;
state = State::Ready;
queue(autoStartEnabled ? Event{0, 0, true, false} : Event{EventStateReady, 0, false, false});
return SCE_OK;
}
int Player::StreamCount() {
std::lock_guard lock(mutex);
if (!source) return SCE_AVPLAYER_ERROR_OPERATION_FAILED;
return static_cast<int>(source->StreamCount());
}
int Player::GetStreamInfo(std::uint32_t index, AvPlayerStreamInfo& info) {
std::lock_guard lock(mutex);
if (!source || !source->GetStreamInfo(index, info)) return SCE_AVPLAYER_ERROR_OPERATION_FAILED;
return SCE_OK;
}
int Player::GetStreamInfoEx(std::uint32_t index, AvPlayerStreamInfoEx& info) {
std::lock_guard lock(mutex);
AvPlayerStreamInfoEx described{};
if (!source || !source->GetStreamInfoEx(index, described)) return SCE_AVPLAYER_ERROR_OPERATION_FAILED;
described.this_size = info.this_size;
info = described;
return SCE_OK;
}
int Player::EnableStream(std::uint32_t index) {
std::lock_guard lock(mutex);
if (!source || !source->EnableStream(index)) return SCE_AVPLAYER_ERROR_OPERATION_FAILED;
return SCE_OK;
}
int Player::DisableStream(std::uint32_t index) {
std::lock_guard lock(mutex);
if (!source || !source->DisableStream(index)) return SCE_AVPLAYER_ERROR_OPERATION_FAILED;
return SCE_OK;
}
int Player::ChangeStream(std::uint32_t from, std::uint32_t to) {
std::lock_guard lock(mutex);
if (!source || state == State::Error || !source->ChangeStream(from, to)) return SCE_AVPLAYER_ERROR_OPERATION_FAILED;
return SCE_OK;
}
int Player::Start() {
std::lock_guard lock(mutex);
if (!source) return SCE_AVPLAYER_ERROR_OPERATION_FAILED;
if (state != State::Ready && state != State::Stop && stopLocked() != SCE_OK) return SCE_AVPLAYER_ERROR_OPERATION_FAILED;
const int result = source->Start();
if (result != SCE_OK) {
previous = state;
state = State::Error;
return result;
}
previous = state;
state = State::Play;
queue({EventStatePlay, 0, false, false});
return SCE_OK;
}
int Player::stopLocked() {
if (!source || state == State::Initial || state == State::Ready || state == State::Stop) return SCE_AVPLAYER_ERROR_OPERATION_FAILED;
source->Stop();
previous = state;
state = State::Stop;
queue({EventStateStop, 0, false, false});
return SCE_OK;
}
int Player::Stop() {
std::lock_guard lock(mutex);
return stopLocked();
}
int Player::Pause() {
std::lock_guard lock(mutex);
if (state == State::EndOfFile) return SCE_OK;
if (!source || state != State::Play) return SCE_AVPLAYER_ERROR_OPERATION_FAILED;
source->Pause();
previous = state;
state = State::Pause;
queue({EventStatePause, 0, false, false});
return SCE_OK;
}
int Player::Resume() {
std::lock_guard lock(mutex);
if (!source || state != State::Pause) return SCE_AVPLAYER_ERROR_OPERATION_FAILED;
source->Resume();
std::swap(state, previous);
queue({EventStatePlay, 0, false, false});
return SCE_OK;
}
int Player::JumpToTime(std::uint64_t milliseconds) {
std::lock_guard lock(mutex);
if (!source || (state != State::Ready && state != State::Play && state != State::Pause)) return SCE_AVPLAYER_ERROR_OPERATION_FAILED;
source->Jump(milliseconds);
return SCE_OK;
}
int Player::SetLooping(bool enabled) {
std::lock_guard lock(mutex);
looping = enabled;
if (source) source->SetLooping(enabled);
return SCE_OK;
}
int Player::SetTrickSpeed(std::int32_t trickSpeed) {
if (trickSpeed == 0) return SCE_AVPLAYER_ERROR_INVALID_PARAMS;
std::lock_guard lock(mutex);
if (state == State::Stop || state == State::EndOfFile || state == State::Error) return SCE_AVPLAYER_ERROR_OPERATION_FAILED;
speed = trickSpeed;
if (source) source->SetSpeed(trickSpeed);
return SCE_OK;
}
int Player::SetAvSyncMode(std::uint32_t mode) {
if (mode != SyncModeDefault && mode != SyncModeNone) return SCE_AVPLAYER_ERROR_INVALID_PARAMS;
std::lock_guard lock(mutex);
syncMode = mode;
if (source) source->SetSyncMode(mode);
return SCE_OK;
}
bool Player::GetVideoData(AvPlayerFrameInfoEx& info) {
std::lock_guard lock(mutex);
if (!source || state != State::Play) return false;
return source->GetVideoData(info);
}
bool Player::GetVideoData(AvPlayerFrameInfo& info) {
AvPlayerFrameInfoEx extended{};
if (!GetVideoData(extended)) return false;
info = {};
info.p_data = static_cast<std::uint8_t*>(extended.p_data);
info.timestamp = extended.timestamp;
info.details.video.width = extended.details.video.width;
info.details.video.height = extended.details.video.height;
info.details.video.aspect_ratio = extended.details.video.aspect_ratio;
std::memcpy(info.details.video.language_code, extended.details.video.language_code, sizeof(info.details.video.language_code));
return true;
}
bool Player::GetAudioData(AvPlayerFrameInfo& info) {
std::lock_guard lock(mutex);
if (!source || state != State::Play) return false;
return source->GetAudioData(info);
}
bool Player::IsActive() {
std::lock_guard lock(mutex);
if (!source || state == State::Initial || state == State::Stop || state == State::EndOfFile || state == State::Error) return false;
return !source->Finished();
}
std::uint64_t Player::CurrentTime() {
std::lock_guard lock(mutex);
return source ? source->CurrentTime() : 0;
}
}