src/transcoding/transcode_io_handler.cc
Rama master · réplica de lectura del repositorio original.
/*GRB*
Gerbera - https://gerbera.io/
transcode_io_handler.cc - this file is part of Gerbera.
Copyright (C) 2026 Gerbera Contributors
Gerbera is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License version 2
as published by the Free Software Foundation.
Gerbera is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Gerbera. If not, see <http://www.gnu.org/licenses/>.
$Id$
*/
// Based in https://github.com/FFmpeg/FFmpeg/blob/master/doc/examples/transcode.c
/*
* Copyright (c) 2010 Nicolas George
* Copyright (c) 2011 Stefano Sabatini
* Copyright (c) 2014 Andrey Utkin
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
* THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
/// @file transcoding/transcode_io_handler.cc
#define GRB_LOG_FAC GrbLogFacility::transcoding
#include "transcode_io_handler.h"
#ifdef HAVE_FFMPEG
#include "config/result/transcoding.h"
#include "exceptions.h"
#include "transcoding/transcode_int_util.h"
#include "upnp/compat.h"
#include "util/logger.h"
#include "util/tools.h"
extern "C" {
#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
}
#include "util/ffmpeg_compat.h"
#include <cstring>
TranscodeInternalIOHandler::TranscodeInternalIOHandler(
const fs::path& filename,
fs::path tempPath,
const std::shared_ptr<TranscodingProfile>& profile,
const std::shared_ptr<CdsObject>& obj,
off_t offset)
: file(filename)
, tmpDir(std::move(tempPath))
, profile(profile)
, obj(obj)
, offset(offset)
{
log_debug("path = {}", file.getPath().string());
}
TranscodeInternalIOHandler::~TranscodeInternalIOHandler()
{
log_debug("cleanup");
av_packet_free(&packet);
avformat_close_input(&inFormatContext);
if (outFormatContext && outFormatContext->pb) {
uint8_t* buffer = nullptr;
int size = avio_close_dyn_buf(outFormatContext->pb, &buffer);
if (size < 0)
log_error("Close dyn buffer failed (error '{}')", av_err2str(size));
av_free(buffer);
outFormatContext->pb = nullptr;
}
avformat_free_context(outFormatContext);
}
void TranscodeInternalIOHandler::open(enum UpnpOpenFileMode mode)
{
log_debug("open {}", file.getPath().string());
if (mode != UPNP_READ)
throw_std_runtime_error("UpnpOpenFileMode {} not supported", mode);
std::lock_guard<std::mutex> lock(mutex);
if (open_input_file() < 0)
throw_std_runtime_error("open input {} failed", file.getPath().string());
outFilePath = tmpDir / fmt::format("grb-tr-{}.{}", generateRandomId(), profile->encoder.getFormat());
if (open_output_file(outFilePath) < 0)
throw_std_runtime_error("open output for {} failed", file.getPath().string());
// https://www.ffmpeg.org/doxygen/8.0/group__lavf__decoding.html#ga3b40fc8d2fda6992ae6ea2567d71ba30
// int avformat_seek_file(AVFormatContext* s, int stream_index, int64_t min_ts, int64_t ts, int64_t max_ts, int flags)
if (offset > 0 && avformat_seek_file(inFormatContext, -1, offset, offset, offset, AVSEEK_FLAG_BYTE) != 0)
throw_std_runtime_error("seek to {} failed", offset);
if (init_filters())
throw_std_runtime_error("init filters for {} failed", file.getPath().string());
packet = av_packet_alloc();
if (!packet)
throw_std_runtime_error("alloc packet for {} failed", file.getPath().string());
}
grb_read_t TranscodeInternalIOHandler::read(std::byte* buf, std::size_t length)
{
std::lock_guard<std::mutex> lock(mutex);
log_debug("read {} {}", file.getPath().string(), length);
std::size_t index = 0;
if (extraData.size() > 0) {
std::size_t bytesToWrite = length > extraData.size() ? extraData.size() : length;
std::memcpy(buf, extraData.data(), bytesToWrite);
index += bytesToWrite;
length -= bytesToWrite;
if (length == 0) {
if (extraData.size() > bytesToWrite)
extraData = std::vector<std::byte>(extraData.data() + bytesToWrite, extraData.data() + extraData.size());
else
extraData.clear();
return bytesToWrite;
}
}
if (isEOF) {
if (index > 0) {
return grb_read_t(index); // successful read
}
return GRB_READ_END;
}
extraData.clear();
while (length > 0) {
auto ret = filterPacket();
if (ret == AVERROR_EOF || ret == AVERROR(EAGAIN)) {
ret = closeOutput();
if (ret < 0) {
log_error("Error occurred: {}", av_err2str(ret));
}
isEOF = true;
}
if (ret < 0)
return GRB_READ_ERROR;
auto buffer = readBuffer();
std::size_t bytesToWrite = length > buffer.size() ? buffer.size() : length;
if (buffer.size() > 0)
std::memcpy(buf + index, buffer.data(), bytesToWrite);
index += bytesToWrite;
length -= bytesToWrite;
if (length == 0) {
if (buffer.size() > bytesToWrite)
extraData = std::vector<std::byte>(buffer.data() + bytesToWrite, buffer.data() + buffer.size());
return grb_read_t(index); // successful read
}
if (isEOF) {
if (index > 0) {
return grb_read_t(index); // successful read
}
return GRB_READ_END;
}
}
return GRB_READ_ERROR; // length is 0 without reading data
}
void TranscodeInternalIOHandler::seek(off_t offset, int whence)
{
std::lock_guard<std::mutex> lock(mutex);
if (!inFormatContext || !inFormatContext->pb)
return;
// https://www.ffmpeg.org/doxygen/8.0/avio_8h.html#a03e23bf0144030961c34e803c71f614f
if (avio_seek(inFormatContext->pb, offset, whence) < 0) {
throw_std_runtime_error("avio_seek failed");
}
extraData.clear();
}
off_t TranscodeInternalIOHandler::tell()
{
std::lock_guard<std::mutex> lock(mutex);
if (inFormatContext && inFormatContext->pb)
return avio_tell(inFormatContext->pb);
return -1;
}
void TranscodeInternalIOHandler::close()
{
}
int TranscodeInternalIOHandler::open_input_file()
{
inFormatContext = nullptr;
log_debug("start '{}'", file.getPath().c_str());
int ret = avformat_open_input(&inFormatContext, file.getPath().c_str(), nullptr, nullptr);
if (ret < 0) {
log_error("Cannot open input file {} (error '{}')", file.getPath().c_str(), av_err2str(ret));
return ret;
}
ret = avformat_find_stream_info(inFormatContext, nullptr);
if (ret < 0) {
log_error("Cannot find stream information in {} (error '{}')", file.getPath().c_str(), av_err2str(ret));
return ret;
}
for (std::size_t streamNumber = 0; streamNumber < inFormatContext->nb_streams; streamNumber++) {
auto sCtx = std::make_shared<StreamContext>(inFormatContext->streams[streamNumber], streamNumber, inFormatContext, profile);
if (!sCtx || !(*sCtx)) {
log_error("Cannot create context for #{}", streamNumber);
return AVERROR_UNKNOWN;
}
streamCtx.emplace(streamNumber, std::move(sCtx));
}
av_dump_format(inFormatContext, 0, file.getPath().c_str(), 0);
log_debug("end '{}'", file.getPath().c_str());
return ret;
}
int TranscodeInternalIOHandler::open_output_file(const std::string& filename)
{
int ret;
log_debug("start '{}'", filename);
outFormatContext = nullptr;
if (filename.empty())
ret = avformat_alloc_output_context2(&outFormatContext, nullptr, profile->encoder.getFormat().c_str(), nullptr);
else
ret = avformat_alloc_output_context2(&outFormatContext, nullptr, profile->encoder.getFormat().c_str(), filename.c_str());
if (ret < 0) {
log_error("Could not create output context for '{}' (error '{}')", filename, av_err2str(ret));
return ret;
}
for (std::size_t streamNumber = 0; streamNumber < inFormatContext->nb_streams; streamNumber++) {
AVStream* out_stream = avformat_new_stream(outFormatContext, nullptr);
if (!out_stream) {
log_error("Failed allocating output stream #{}", streamNumber);
return AVERROR_UNKNOWN;
}
ret = streamCtx.at(streamNumber)->createEncoder(out_stream, outFormatContext);
if (ret < 0) {
log_error("Could not create output encoder for '{}'", filename);
return ret;
}
}
if (!filename.empty()) {
av_dump_format(outFormatContext, 0, filename.c_str(), 1);
if (!(outFormatContext->oformat->flags & AVFMT_NOFILE)) {
ret = avio_open_dyn_buf(&outFormatContext->pb);
if (ret >= 0)
outFormatContext->pb->seekable = 0;
}
if (ret < 0) {
log_error("Could not open output file '{}' (error '{}')", filename, av_err2str(ret));
return ret;
}
AVDictionary* muxOpts = nullptr;
const auto& muxOptStr = profile->encoder.getMuxOpts();
if (!muxOptStr.empty()) {
ret = av_dict_parse_string(&muxOpts, muxOptStr.c_str(), "=", ":", 0);
if (ret < 0) {
log_error("Invalid muxopts '{}' (error '{}')", muxOptStr, av_err2str(ret));
av_dict_free(&muxOpts);
return ret;
}
}
/* init muxer, write output file header */
ret = avformat_write_header(outFormatContext, &muxOpts);
const AVDictionaryEntry* e = nullptr;
while ((e = av_dict_get(muxOpts, "", e, AV_DICT_IGNORE_SUFFIX)))
log_warning("Unused muxer option '{}={}'", e->key, e->value);
av_dict_free(&muxOpts);
if (ret < 0) {
log_error("Error occurred when opening output file '{}' (error '{}')", filename, av_err2str(ret));
return ret;
}
} else {
ret = avformat_init_output(outFormatContext, nullptr);
if (ret < 0) {
log_error("Could not open output stream (error '{}')", av_err2str(ret));
return ret;
}
}
return ret;
}
int TranscodeInternalIOHandler::init_filters(void)
{
log_debug("start");
for (std::size_t i = 0; i < inFormatContext->nb_streams; i++) {
if (!(as_codecpar(inFormatContext->streams[i])->codec_type == AVMEDIA_TYPE_AUDIO
|| as_codecpar(inFormatContext->streams[i])->codec_type == AVMEDIA_TYPE_VIDEO))
continue;
std::string filter_spec;
if (as_codecpar(inFormatContext->streams[i])->codec_type == AVMEDIA_TYPE_VIDEO) {
filter_spec = (!profile->encoder.getVFilter().empty())
? profile->encoder.getVFilter()
: "null"; /* passthrough (dummy) filter for video */
} else {
filter_spec = (!profile->encoder.getAFilter().empty())
? profile->encoder.getAFilter()
: "anull"; /* passthrough (dummy) filter for audio */
}
auto fCtx = std::make_shared<FilteringContext>(streamCtx.at(i)->getDecoder(), streamCtx.at(i)->getEncoder(), i, filter_spec, profile);
filteringCtx.emplace(i, std::move(fCtx));
}
log_debug("end");
return 0;
}
int TranscodeInternalIOHandler::flush_encoder(std::size_t stream_index)
{
if (!(streamCtx.at(stream_index)->getEncoder()->codec->capabilities & AV_CODEC_CAP_DELAY))
return 0;
log_debug("Flushing stream #{} encoder", stream_index);
return filteringCtx.at(stream_index)->encodeWriteFrame(outFormatContext, true);
}
int TranscodeInternalIOHandler::filterPacket()
{
int ret = av_read_frame(inFormatContext, packet);
if (ret < 0) {
log_debug("(error '{}')", av_err2str(ret));
return AVERROR(EAGAIN);
}
std::size_t stream_index = packet->stream_index;
log_debug("Demuxer gave frame of stream_index #{}", stream_index);
auto filter = filteringCtx.at(stream_index);
if (filter && *filter) {
auto stream = streamCtx.at(stream_index);
log_debug("Going to reencode&filter the frame");
ret = stream->sendPacket(packet);
if (ret < 0) {
log_error("Decoding failed (error '{}')", av_err2str(ret));
return AVERROR(EAGAIN);
}
while (ret >= 0) {
ret = stream->receiveFrame();
if (ret == AVERROR_EOF || ret == AVERROR(EAGAIN))
break;
else if (ret < 0)
return ret;
stream->dec_frame->pts = stream->dec_frame->best_effort_timestamp;
ret = filter->filterEncodeWriteFrame(stream->dec_frame, outFormatContext);
if (ret < 0)
return ret;
}
} else {
/* remux this frame without reencoding */
av_packet_rescale_ts(packet,
inFormatContext->streams[stream_index]->time_base,
outFormatContext->streams[stream_index]->time_base);
ret = av_interleaved_write_frame(outFormatContext, packet);
if (ret < 0) {
log_debug("Flushing av_interleaved_write_frame (error '{}')", av_err2str(ret));
return ret;
}
}
av_packet_unref(packet);
return 0;
}
int TranscodeInternalIOHandler::run(const std::string& outputFileName)
{
int ret = open_input_file();
if (ret < 0)
return ret;
ret = open_output_file(outputFileName);
if (ret < 0)
return ret;
ret = init_filters();
if (ret < 0)
return ret;
packet = av_packet_alloc();
if (!packet)
return -1;
/* read all packets */
while (1) {
ret = filterPacket();
if (ret == AVERROR_EOF || ret == AVERROR(EAGAIN))
break;
else if (ret < 0)
return ret;
}
ret = closeOutput();
if (ret < 0) {
log_debug("Error occurred: {}", av_err2str(ret));
}
return ret ? 1 : 0;
}
int TranscodeInternalIOHandler::closeOutput()
{
int ret = 0;
/* flush decoders, filters and encoders */
for (std::size_t i = 0; i < inFormatContext->nb_streams; i++) {
auto filter = filteringCtx.at(i);
if (!filter || !*filter)
continue;
auto inputContext = streamCtx.at(i);
log_debug("Flushing stream #{} decoder", i);
/* flush decoder */
ret = inputContext->sendPacket(nullptr);
if (ret < 0) {
log_error("Flushing decoding failed (error '{}')", av_err2str(ret));
return ret;
}
while (ret >= 0) {
ret = inputContext->receiveFrame();
if (ret == AVERROR_EOF)
break;
else if (ret < 0)
return ret;
inputContext->dec_frame->pts = inputContext->dec_frame->best_effort_timestamp;
ret = filter->filterEncodeWriteFrame(inputContext->dec_frame, outFormatContext);
if (ret < 0)
return ret;
}
/* flush filter */
ret = filter->filterEncodeWriteFrame(nullptr, outFormatContext);
if (ret < 0) {
log_error("Flushing filter failed (error '{}')", av_err2str(ret));
return ret;
}
/* flush encoder */
ret = flush_encoder(i);
if (ret < 0) {
log_error("Flushing encoder failed (error '{}')", av_err2str(ret));
return ret;
}
}
av_write_trailer(outFormatContext);
return ret;
}
std::vector<std::byte> TranscodeInternalIOHandler::readBuffer()
{
uint8_t* buffer = nullptr;
std::vector<std::byte> result;
log_debug("Fetching Result from buffer");
if (!outFormatContext || !outFormatContext->pb)
return result;
int size = avio_close_dyn_buf(outFormatContext->pb, &buffer);
outFormatContext->pb = nullptr;
if (size >= 0) {
if (size > 0)
result = std::vector<std::byte>((std::byte*)buffer, (std::byte*)(buffer + size));
log_debug("Got {} bytes from buffer", size);
} else {
log_error("Close dyn buffer failed (error '{}')", av_err2str(size));
}
av_free(buffer);
int ret = avio_open_dyn_buf(&outFormatContext->pb);
if (ret < 0)
log_error("Open dyn buffer failed (error '{}')", av_err2str(ret));
else
outFormatContext->pb->seekable = 0;
return result;
}
#endif