mirror of
https://github.com/steredbits/pelagia.git
synced 2026-10-18 20:01:34 +02:00
264 lines
7.9 KiB
C++
264 lines
7.9 KiB
C++
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
// Copyright (C) 2026 steredbits and Pelagia contributors
|
|
#include "player/decoder.h"
|
|
|
|
#include <chrono>
|
|
|
|
#include "util/log.h"
|
|
|
|
namespace player {
|
|
|
|
namespace {
|
|
|
|
double chrono_now_ms() {
|
|
const auto now = std::chrono::steady_clock::now().time_since_epoch();
|
|
return std::chrono::duration<double, std::milli>(now).count();
|
|
}
|
|
|
|
} // namespace
|
|
|
|
// --- VideoDecoder -----------------------------------------------------------
|
|
|
|
bool VideoDecoder::open(const AVStream* stream) {
|
|
return open(stream->codecpar, stream->time_base, stream->avg_frame_rate);
|
|
}
|
|
|
|
bool VideoDecoder::open(const AVCodecParameters* par, AVRational time_base,
|
|
AVRational frame_rate) {
|
|
const AVCodec* codec = avcodec_find_decoder(par->codec_id);
|
|
if (!codec) {
|
|
LOG_ERROR("VideoDecoder: codec %d not supported", static_cast<int>(par->codec_id));
|
|
return false;
|
|
}
|
|
codec_ = avcodec_alloc_context3(codec);
|
|
if (!codec_ || avcodec_parameters_to_context(codec_, par) < 0 ||
|
|
avcodec_open2(codec_, codec, nullptr) < 0) {
|
|
LOG_ERROR("VideoDecoder: cannot open codec %s", codec->name);
|
|
close();
|
|
return false;
|
|
}
|
|
time_base_ = time_base;
|
|
const AVRational fr = frame_rate;
|
|
frame_duration_ms_ = (fr.num > 0) ? (1000LL * fr.den / fr.num) : 40;
|
|
last_pts_ms_ = 0;
|
|
LOG_INFO("VideoDecoder : %s %dx%d", codec->name, codec_->width, codec_->height);
|
|
return true;
|
|
}
|
|
|
|
void VideoDecoder::close() {
|
|
if (sws_) {
|
|
sws_freeContext(sws_);
|
|
sws_ = nullptr;
|
|
}
|
|
if (codec_) {
|
|
avcodec_free_context(&codec_);
|
|
}
|
|
}
|
|
|
|
int VideoDecoder::send(const AVPacket* pkt) {
|
|
const double t0 = chrono_now_ms();
|
|
const int err = avcodec_send_packet(codec_, pkt);
|
|
busy_ms_ += chrono_now_ms() - t0;
|
|
return err;
|
|
}
|
|
|
|
AVFrame* VideoDecoder::receive(int* status) {
|
|
AVFrame* frame = av_frame_alloc();
|
|
const double t0 = chrono_now_ms();
|
|
const int err = avcodec_receive_frame(codec_, frame);
|
|
if (err < 0) {
|
|
busy_ms_ += chrono_now_ms() - t0;
|
|
av_frame_free(&frame);
|
|
*status = err;
|
|
return nullptr;
|
|
}
|
|
|
|
if (frame->format != AV_PIX_FMT_YUV420P) {
|
|
// Conversion to YUV420p - only if the source is not already in it
|
|
// (regular 8-bit H.264 outputs YUV420p natively).
|
|
AVFrame* converted = av_frame_alloc();
|
|
converted->format = AV_PIX_FMT_YUV420P;
|
|
converted->width = frame->width;
|
|
converted->height = frame->height;
|
|
if (av_frame_get_buffer(converted, 0) < 0) {
|
|
LOG_ERROR("VideoDecoder: cannot allocate the converted frame");
|
|
av_frame_free(&converted);
|
|
av_frame_free(&frame);
|
|
busy_ms_ += chrono_now_ms() - t0;
|
|
*status = AVERROR(ENOMEM);
|
|
return nullptr;
|
|
}
|
|
sws_ = sws_getCachedContext(sws_, frame->width, frame->height,
|
|
static_cast<AVPixelFormat>(frame->format),
|
|
frame->width, frame->height, AV_PIX_FMT_YUV420P,
|
|
SWS_BILINEAR, nullptr, nullptr, nullptr);
|
|
sws_scale(sws_, frame->data, frame->linesize, 0, frame->height,
|
|
converted->data, converted->linesize);
|
|
converted->pts = frame->pts;
|
|
converted->best_effort_timestamp = frame->best_effort_timestamp;
|
|
av_frame_free(&frame);
|
|
frame = converted;
|
|
}
|
|
|
|
busy_ms_ += chrono_now_ms() - t0;
|
|
*status = 0;
|
|
return frame;
|
|
}
|
|
|
|
void VideoDecoder::flush() {
|
|
if (codec_) {
|
|
avcodec_flush_buffers(codec_);
|
|
}
|
|
}
|
|
|
|
int64_t VideoDecoder::frame_pts_ms(const AVFrame* frame) {
|
|
int64_t pts = frame->best_effort_timestamp;
|
|
if (pts == AV_NOPTS_VALUE) {
|
|
pts = frame->pts;
|
|
}
|
|
if (pts == AV_NOPTS_VALUE) {
|
|
last_pts_ms_ += frame_duration_ms_;
|
|
return last_pts_ms_;
|
|
}
|
|
last_pts_ms_ = av_rescale_q(pts, time_base_, AVRational{1, 1000});
|
|
return last_pts_ms_;
|
|
}
|
|
|
|
// --- AudioDecoder -----------------------------------------------------------
|
|
|
|
bool AudioDecoder::open(const AVStream* stream, int out_sample_rate,
|
|
int out_channels) {
|
|
return open(stream->codecpar, stream->time_base, out_sample_rate, out_channels);
|
|
}
|
|
|
|
bool AudioDecoder::open(const AVCodecParameters* par, AVRational time_base,
|
|
int out_sample_rate, int out_channels) {
|
|
const AVCodec* codec = avcodec_find_decoder(par->codec_id);
|
|
if (!codec) {
|
|
LOG_ERROR("AudioDecoder: codec %d not supported", static_cast<int>(par->codec_id));
|
|
return false;
|
|
}
|
|
codec_ = avcodec_alloc_context3(codec);
|
|
if (!codec_ || avcodec_parameters_to_context(codec_, par) < 0 ||
|
|
avcodec_open2(codec_, codec, nullptr) < 0) {
|
|
LOG_ERROR("AudioDecoder: cannot open codec %s", codec->name);
|
|
close();
|
|
return false;
|
|
}
|
|
time_base_ = time_base;
|
|
out_sample_rate_ = out_sample_rate;
|
|
out_channels_ = out_channels;
|
|
|
|
AVChannelLayout out_layout;
|
|
av_channel_layout_default(&out_layout, out_channels);
|
|
AVChannelLayout in_layout = codec_->ch_layout;
|
|
if (in_layout.nb_channels == 0) {
|
|
av_channel_layout_default(&in_layout, 2);
|
|
}
|
|
const int err = swr_alloc_set_opts2(
|
|
&swr_, &out_layout, AV_SAMPLE_FMT_S16, out_sample_rate, &in_layout,
|
|
codec_->sample_fmt, codec_->sample_rate, 0, nullptr);
|
|
if (err < 0 || swr_init(swr_) < 0) {
|
|
LOG_ERROR("AudioDecoder: swresample initialization failed");
|
|
close();
|
|
return false;
|
|
}
|
|
next_pts_ms_ = 0;
|
|
LOG_INFO("AudioDecoder : %s %d Hz %d canaux -> S16 %d Hz %d canaux",
|
|
codec->name, codec_->sample_rate, codec_->ch_layout.nb_channels,
|
|
out_sample_rate, out_channels);
|
|
return true;
|
|
}
|
|
|
|
void AudioDecoder::close() {
|
|
if (swr_) {
|
|
swr_free(&swr_);
|
|
}
|
|
if (codec_) {
|
|
avcodec_free_context(&codec_);
|
|
}
|
|
}
|
|
|
|
int AudioDecoder::send(const AVPacket* pkt) {
|
|
const double t0 = chrono_now_ms();
|
|
const int err = avcodec_send_packet(codec_, pkt);
|
|
busy_ms_ += chrono_now_ms() - t0;
|
|
return err;
|
|
}
|
|
|
|
bool AudioDecoder::convert_frame(const AVFrame* frame, AudioChunk* out) {
|
|
const int in_samples = frame ? frame->nb_samples : 0;
|
|
const int64_t max_out = av_rescale_rnd(
|
|
swr_get_delay(swr_, codec_->sample_rate) + in_samples, out_sample_rate_,
|
|
codec_->sample_rate > 0 ? codec_->sample_rate : out_sample_rate_,
|
|
AV_ROUND_UP);
|
|
if (max_out <= 0) {
|
|
return false;
|
|
}
|
|
const size_t bytes_per_sample = sizeof(int16_t) * out_channels_;
|
|
uint8_t* buf = static_cast<uint8_t*>(av_malloc(max_out * bytes_per_sample));
|
|
if (!buf) {
|
|
return false;
|
|
}
|
|
const uint8_t** in_data =
|
|
frame ? const_cast<const uint8_t**>(frame->extended_data) : nullptr;
|
|
const int got = swr_convert(swr_, &buf, static_cast<int>(max_out), in_data,
|
|
in_samples);
|
|
if (got <= 0) {
|
|
av_free(buf);
|
|
return false;
|
|
}
|
|
|
|
out->data = buf;
|
|
out->bytes = static_cast<size_t>(got) * bytes_per_sample;
|
|
out->duration_ms = 1000LL * got / out_sample_rate_;
|
|
|
|
int64_t pts = frame ? frame->best_effort_timestamp : AV_NOPTS_VALUE;
|
|
if (frame && pts == AV_NOPTS_VALUE) {
|
|
pts = frame->pts;
|
|
}
|
|
if (pts != AV_NOPTS_VALUE) {
|
|
out->pts_ms = av_rescale_q(pts, time_base_, AVRational{1, 1000});
|
|
} else {
|
|
out->pts_ms = next_pts_ms_;
|
|
}
|
|
next_pts_ms_ = out->pts_ms + out->duration_ms;
|
|
return true;
|
|
}
|
|
|
|
bool AudioDecoder::receive(AudioChunk* out, int* status) {
|
|
AVFrame* frame = av_frame_alloc();
|
|
const double t0 = chrono_now_ms();
|
|
const int err = avcodec_receive_frame(codec_, frame);
|
|
if (err < 0) {
|
|
// At the end of the drain, flushes what is left in swresample.
|
|
bool got_tail = false;
|
|
if (err == AVERROR_EOF) {
|
|
got_tail = convert_frame(nullptr, out);
|
|
}
|
|
busy_ms_ += chrono_now_ms() - t0;
|
|
av_frame_free(&frame);
|
|
*status = got_tail ? 0 : err;
|
|
return got_tail;
|
|
}
|
|
const bool ok = convert_frame(frame, out);
|
|
busy_ms_ += chrono_now_ms() - t0;
|
|
av_frame_free(&frame);
|
|
*status = ok ? 0 : AVERROR(EAGAIN);
|
|
return ok;
|
|
}
|
|
|
|
void AudioDecoder::flush() {
|
|
if (codec_) {
|
|
avcodec_flush_buffers(codec_);
|
|
}
|
|
// Starts again from a clean converter: pending samples date
|
|
// from before the seek.
|
|
if (swr_) {
|
|
swr_close(swr_);
|
|
swr_init(swr_);
|
|
}
|
|
}
|
|
|
|
} // namespace player
|