Пакетирование аудио данных типа pcm_alaw в mka аудиофайл, используя ffmpeg API
UPD
Представим, что в моем проекте, я получаю RTP пакеты с типом полезной нагрузки-8, для последующего сохранения данной нагрузки в качестве N-й части аудиодорожки. Я извлекаю эту нагрузку из пакета RTP и сохраняю ее во временный буфер:
...
while ((rtp = receiveRtpPackets()).withoutErrors()) {
payloadData.push(rtp.getPayloadData());
}
audioGenerator.setPayloadData(payloadData);
audioGenerator.recordToFile();
...
После заполнения временного буфера определенного размера данной полезной нагрузкой, я обрабатываю этот буфер, а именно, извлекаю всю полезную нагрузку и кодирую с использованием ffmpeg для дальнейшего сохранения в аудиофайл, формата Matroska. Но у меня есть проблема. Поскольку полезная нагрузка пакета RTP имеет тип 8, я должен сохранить сырые аудиоданные формата pcm_alaw в mka формат аудио. Но при сохранение сырых данный pcm_alaw в аудиофайл, я получаю такие вот сообщения, со стороны библиотеки:
...
[libopus @ 0x18eff60] Queue input is backward in time
[libopus @ 0x18eff60] Queue input is backward in time
[libopus @ 0x18eff60] Queue input is backward in time
[libopus @ 0x18eff60] Queue input is backward in time
...
При открытии аудиофайла в vlc, ничего не воспроизводится (временная метка аудиодорожки отсутствует).
Задача моего проекта заключается в том, чтобы просто взять pcm_alaw данные и запаковать их в контейнер, формата mka. Благодаря комментарию пользователя Fat-Zer, было определено, что лучше всего использовать для определения кодека - av_guess_codec() функцию, которая в свою очередь, автоматически подбирает нужный идентификатор кодека. Но как мне запаковать сырые данные в контейнер правильно, я не знаю.
Важно отметить, что я могу получить в качестве сырых данных любой формат этих данных (только аудио форматы), определенный по типу RTP пакета (Все типы полезной нагрузки RTP пакета). Известно лишь то, что в любом случае, я должен упаковать аудиоданные в mka контейнер.
Прилагаю и код (позаимствовал с этого ресурса), который я использую:
audiogenerater.h
extern "C"
{
#include "libavformat/avformat.h"
#include "libavcodec/avcodec.h"
#include "libswresample/swresample.h"
}
class AudioGenerater
{
public:
AudioGenerater();
~AudioGenerater() = default;
void generateAudioFileWithOptions(
QString fileName,
QByteArray pcmData,
int channel,
int bitRate,
int sampleRate,
AVSampleFormat format);
private:
// init Format
bool initFormat(QString audioFileName);
private:
AVCodec *m_AudioCodec = nullptr;
AVCodecContext *m_AudioCodecContext = nullptr;
AVFormatContext *m_FormatContext = nullptr;
AVOutputFormat *m_OutputFormat = nullptr;
};
audiogenerater.cpp
AudioGenerater::AudioGenerater()
{
av_register_all();
avcodec_register_all();
}
AudioGenerater::~AudioGenerater()
{
// ...
}
bool AudioGenerater::initFormat(QString audioFileName)
{
// Create an output Format context
int result = avformat_alloc_output_context2(&m_FormatContext, nullptr, nullptr, audioFileName.toLocal8Bit().data());
if (result < 0) {
return false;
}
m_OutputFormat = m_FormatContext->oformat;
// Create an audio stream
AVStream* audioStream = avformat_new_stream(m_FormatContext, m_AudioCodec);
if (audioStream == nullptr) {
avformat_free_context(m_FormatContext);
return false;
}
// Set the parameters in the stream
audioStream->id = m_FormatContext->nb_streams - 1;
audioStream->time_base = { 1, 8000 };
result = avcodec_parameters_from_context(audioStream->codecpar, m_AudioCodecContext);
if (result < 0) {
avformat_free_context(m_FormatContext);
return false;
}
// Print FormatContext information
av_dump_format(m_FormatContext, 0, audioFileName.toLocal8Bit().data(), 1);
// Open file IO
if (!(m_OutputFormat->flags & AVFMT_NOFILE)) {
result = avio_open(&m_FormatContext->pb, audioFileName.toLocal8Bit().data(), AVIO_FLAG_WRITE);
if (result < 0) {
avformat_free_context(m_FormatContext);
return false;
}
}
return true;
}
void AudioGenerater::generateAudioFileWithOptions(
QString _fileName,
QByteArray _pcmData,
int _channel,
int _bitRate,
int _sampleRate,
AVSampleFormat _format)
{
AVFormatContext* oc;
if (avformat_alloc_output_context2(
&oc, nullptr, nullptr, _fileName.toStdString().c_str())
< 0) {
qDebug() << "Error in line: " << __LINE__;
return;
}
if (!oc) {
printf("Could not deduce output format from file extension: using mka.\n");
avformat_alloc_output_context2(
&oc, nullptr, "mka", _fileName.toStdString().c_str());
}
if (!oc) {
qDebug() << "Error in line: " << __LINE__;
return;
}
AVOutputFormat* fmt = oc->oformat;
if (fmt->audio_codec == AV_CODEC_ID_NONE) {
qDebug() << "Error in line: " << __LINE__;
return;
}
AVCodecID codecID = av_guess_codec(
fmt, nullptr, _fileName.toStdString().c_str(), nullptr, AVMEDIA_TYPE_AUDIO);
// Find Codec
m_AudioCodec = avcodec_find_encoder(codecID);
if (m_AudioCodec == nullptr) {
qDebug() << "Error in line: " << __LINE__;
return;
}
// Create an encoder context
m_AudioCodecContext = avcodec_alloc_context3(m_AudioCodec);
if (m_AudioCodecContext == nullptr) {
qDebug() << "Error in line: " << __LINE__;
return;
}
// Setting parameters
m_AudioCodecContext->bit_rate = _bitRate;
m_AudioCodecContext->sample_rate = _sampleRate;
m_AudioCodecContext->sample_fmt = _format;
m_AudioCodecContext->channels = _channel;
m_AudioCodecContext->channel_layout = av_get_default_channel_layout(_channel);
m_AudioCodecContext->flags |= AV_CODEC_FLAG_GLOBAL_HEADER;
// Turn on the encoder
int result = avcodec_open2(m_AudioCodecContext, m_AudioCodec, nullptr);
if (result < 0) {
avcodec_free_context(&m_AudioCodecContext);
if (m_FormatContext != nullptr)
avformat_free_context(m_FormatContext);
return;
}
// Create a package
if (!initFormat(_fileName)) {
avcodec_free_context(&m_AudioCodecContext);
if (m_FormatContext != nullptr)
avformat_free_context(m_FormatContext);
return;
}
// write to the file header
result = avformat_write_header(m_FormatContext, nullptr);
if (result < 0) {
avcodec_free_context(&m_AudioCodecContext);
if (m_FormatContext != nullptr)
avformat_free_context(m_FormatContext);
return;
}
// Create Frame
AVFrame* frame = av_frame_alloc();
if (frame == nullptr) {
avcodec_free_context(&m_AudioCodecContext);
if (m_FormatContext != nullptr)
avformat_free_context(m_FormatContext);
return;
}
int nb_samples = 0;
if (m_AudioCodecContext->codec->capabilities & AV_CODEC_CAP_VARIABLE_FRAME_SIZE) {
nb_samples = 10000;
}
else {
nb_samples = m_AudioCodecContext->frame_size;
}
// Set the parameters of the Frame
frame->nb_samples = nb_samples;
frame->format = m_AudioCodecContext->sample_fmt;
frame->channel_layout = m_AudioCodecContext->channel_layout;
// Apply for data memory
result = av_frame_get_buffer(frame, 0);
if (result < 0) {
av_frame_free(&frame);
{
avcodec_free_context(&m_AudioCodecContext);
if (m_FormatContext != nullptr)
avformat_free_context(m_FormatContext);
return;
}
}
// Set the Frame to be writable
result = av_frame_make_writable(frame);
if (result < 0) {
av_frame_free(&frame);
{
avcodec_free_context(&m_AudioCodecContext);
if (m_FormatContext != nullptr)
avformat_free_context(m_FormatContext);
return;
}
}
int perFrameDataSize = frame->linesize[0];
int count = _pcmData.size() / perFrameDataSize;
bool needAddOne = false;
if (_pcmData.size() % perFrameDataSize != 0) {
count++;
needAddOne = true;
}
int frameCount = 0;
for (int i = 0; i < count; ++i) {
// Create a Packet
AVPacket* pkt = av_packet_alloc();
if (pkt == nullptr) {
avcodec_free_context(&m_AudioCodecContext);
if (m_FormatContext != nullptr)
avformat_free_context(m_FormatContext);
return;
}
av_init_packet(pkt);
if (i == count - 1)
perFrameDataSize = _pcmData.size() % perFrameDataSize;
// Synthesize WAV files
memset(frame->data[0], 0, perFrameDataSize);
memcpy(frame->data[0], &(_pcmData.data()[perFrameDataSize * i]), perFrameDataSize);
frame->pts = frameCount++;
// send Frame
result = avcodec_send_frame(m_AudioCodecContext, frame);
if (result < 0)
continue;
// Receive the encoded Packet
result = avcodec_receive_packet(m_AudioCodecContext, pkt);
if (result < 0) {
av_packet_free(&pkt);
continue;
}
// write to file
av_packet_rescale_ts(pkt, m_AudioCodecContext->time_base, m_FormatContext->streams[0]->time_base);
pkt->stream_index = 0;
result = av_interleaved_write_frame(m_FormatContext, pkt);
if (result < 0)
continue;
av_packet_free(&pkt);
}
// write to the end of the file
av_write_trailer(m_FormatContext);
// Close file IO
avio_closep(&m_FormatContext->pb);
// Release Frame memory
av_frame_free(&frame);
avcodec_free_context(&m_AudioCodecContext);
if (m_FormatContext != nullptr)
avformat_free_context(m_FormatContext);
}
main.cpp
int main(int argc, char **argv)
{
av_log_set_level(AV_LOG_TRACE);
QFile file("rawDataOfPcmAlawType.bin");
if (!file.open(QIODevice::ReadOnly)) {
return EXIT_FAILURE;
}
QByteArray rawData(file.readAll());
AudioGenerater generator;
generator.generateAudioFileWithOptions(
"test.mka",
rawData,
1,
64000,
8000,
AV_SAMPLE_FMT_S16);
return 0;
}
ВАЖНО Помочь найти самый подходящий вариант записи pcm_alaw или различного от этого формата данных в mka аудиофайл.
UPD Использование FFMPEG как отдельный процесс к сожаление запрешен.
Ответы (2 шт):
А что если запустить ffmpeg как процесс и кормить его буфером alaw через stdin?
dd if=/dev/urandom | ffmpeg -f alaw -i pipe: -c:a copy -f matroska data.mka
Красивый шум пакуется без проблем.
А ещё ffmpeg сам умеет подключаться к rtp. Можно просто держать его в сабпроцессе.
Задача моего проекта заключается в том, чтобы просто взять pcm_alaw данные и запаковать их в контейнер, формата mka.
С такой формулировкой, самым простым вариантом будет скармливать весь поток ffmpeg'у и «демуксить» его встроенной заглушкой для сырого кодека; а потом просто муксить дорожку из потока в mkv «как есть», без перекодирования:
// build : g++ -g3 -O0 -std=c++17 -lavformat -lavutil -lavcodec ./av_muxrecorder.cpp -o muxer
// run : ./muxer rawData foo.mka
#include <iostream>
#include <fstream>
#include <memory>
#include <cassert>
extern "C" {
#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
#include <libavutil/error.h>
#include <libavutil/log.h>
}
#undef av_err2str
#define av_err2str(errnum) \
av_make_error_string((char*)alloca(AV_ERROR_MAX_STRING_SIZE), AV_ERROR_MAX_STRING_SIZE, errnum)
using namespace std::string_literals;
template <>
struct std::default_delete<AVFormatContext> {
void operator()(AVFormatContext* p) const noexcept { avformat_free_context(p); }
};
template <>
struct std::default_delete<AVCodecContext> {
void operator()(AVCodecContext* p) const noexcept { avcodec_free_context(&p); }
};
template <>
struct std::default_delete<AVIOContext> {
void operator()(AVIOContext* p) const noexcept {
if (p) { av_freep(&p->buffer); }
avio_context_free(&p);
}
};
class MuxRecorder {
std::unique_ptr<AVFormatContext> inCtx;
std::unique_ptr<AVIOContext> inIOCtx;
std::unique_ptr<AVCodecContext> inCodecCtx;
AVStream* inStream;
std::shared_ptr<AVFormatContext> outCtx;
AVStream* outStream;
const uint8_t *curData;
size_t curSz;
bool isFinished;
public:
MuxRecorder (AVInputFormat *fmt, AVDictionary **inOpts);
void setOutput (std::shared_ptr<AVFormatContext> outCtx_);
void processData(const uint8_t *data, size_t sz);
void finalize();
protected:
void doProcessStream();
private:
/// служебная функция, используется как callback в AVIOContext
static int readPacket(void *opaque, uint8_t *buf, int sz);
};
MuxRecorder::MuxRecorder (AVInputFormat *fmt, AVDictionary **inOpts)
:curData(0),
curSz(0),
isFinished(0)
{
int rc;
const size_t ioBufSz = 4096; // должен быть не меньше, чем размер пакета кодека,
// может быть самостоятельно увеличен libav
uint8_t *ioCtxBuffer = (uint8_t*)av_malloc(ioBufSz);
if(!ioCtxBuffer) throw std::bad_alloc();
inIOCtx.reset(avio_alloc_context(
/* buffer */ ioCtxBuffer, /* buffer_size */ ioBufSz, /* write_flag */0,
/* user_data */ this,
/* read */ &MuxRecorder::readPacket,
/* write */ 0,
/* seek */ 0));
if (!inIOCtx) {
av_free(ioCtxBuffer);
throw std::bad_alloc();
} // В сллучае успеха inIOCtx принимает владение буфером ioCtxBuffer
/********* Создание контекста для демультиплексера формата ***********/
// для pcml это будет просто заглушка
AVFormatContext *fmtCtx = avformat_alloc_context();
if(!fmtCtx) throw std::bad_alloc();
fmtCtx->pb = inIOCtx.get();
// TODO: для более сложных форматов это должно делаться в processData()
rc = avformat_open_input(&fmtCtx, 0, fmt, inOpts);
if (rc < 0) {
// avformat_open_input() освобождает fmtCtx при неудаче
throw std::runtime_error( "Failed to open input: "s.append(av_err2str(rc)) );
}
inCtx.reset(fmtCtx);
if (inCtx->nb_streams != 1) { // для простого формата поток должен быть один
throw std::runtime_error("Input has several streams");
}
inStream = inCtx->streams[0];
/********* Определение кодека входного потока ************************/
// для сырых кодеков, в частности, pcml это формальность,
// id должен совпдать с fmt->raw_codec_id
AVCodec *inCodec = avcodec_find_decoder(inStream->codecpar->codec_id);
if (!inCodec) {
throw std::runtime_error("Couldn't find codec for input stream");
}
inCodecCtx.reset(avcodec_alloc_context3(inCodec));
if(!inCodecCtx) throw std::bad_alloc();
avcodec_parameters_to_context (inCodecCtx.get(), inStream->codecpar);
rc = avcodec_open2(inCodecCtx.get(), inCodec, inOpts);
if (rc < 0) {
throw std::runtime_error( "Failed to open codec context: "s.append(av_err2str(rc)) );
}
}
void MuxRecorder::setOutput (std::shared_ptr<AVFormatContext> outCtx_) {
outCtx.swap(outCtx_);
// добавить поток данных в файл
outStream = avformat_new_stream(outCtx.get(), NULL);
if(!outStream) { throw std::bad_alloc(); }
int rc = avcodec_parameters_copy(outStream->codecpar, inStream->codecpar);
if (rc < 0) {
throw std::runtime_error("Failed to copy parameters: "s.append(av_err2str(rc)));
}
outStream->codecpar->codec_tag = 0;
}
void MuxRecorder::finalize() {
isFinished = 1;
doProcessStream(); // обработать остаток входных данных
// Закрытие контекста входных данных.
AVFormatContext *fmtCtx = inCtx.release();
avformat_close_input(&fmtCtx);
}
void MuxRecorder::processData(const uint8_t *data, size_t sz) {
// В некотором роде хак т.к. при любой ошибке в readPacket()
// (в том числе и EAGAIN) ffmpeg устанавливает этот флаг в своих нутрах
if(inIOCtx->error == AVERROR(EAGAIN)) {
inIOCtx->eof_reached = 0;
}
curData = data;
curSz = sz;
doProcessStream(); // собственно обработать данные из буыера
assert(curSz == 0); // к этому моменту ffmpeg должен вычитаться всё
}
void MuxRecorder::doProcessStream() {
AVPacket pkt;
while(1) {
int rc = av_read_frame(inCtx.get(), &pkt);
if(rc == AVERROR(EAGAIN) || rc == AVERROR_EOF ) {
break;
} else if (rc<0) {
throw std::runtime_error("Failed to read packet: "s.append(av_err2str(rc)));
} else if(pkt.stream_index == inStream->index) { //< проверка для галочки
/* copy packet */
pkt.pts = av_rescale_q_rnd( pkt.pts, inStream->time_base, outStream->time_base,
(enum AVRounding) (AV_ROUND_NEAR_INF | AV_ROUND_PASS_MINMAX) );
pkt.dts = av_rescale_q_rnd( pkt.dts, inStream->time_base, outStream->time_base,
(enum AVRounding) (AV_ROUND_NEAR_INF | AV_ROUND_PASS_MINMAX) );
pkt.duration = av_rescale_q(pkt.duration, inStream->time_base, outStream->time_base);
pkt.pos = -1;
rc = av_interleaved_write_frame(outCtx.get(), &pkt);
if (rc < 0) {
av_packet_unref(&pkt);
throw std::runtime_error("Failed to mux packet into output stream: "s
.append(av_err2str(rc)));
}
av_packet_unref(&pkt);
}
}
}
int MuxRecorder::readPacket(void *opaque, uint8_t *buf, int sz) {
MuxRecorder* self = (MuxRecorder*) opaque;
if(self->curData && self->curSz) {
sz = std::min(sz,(int) self->curSz);
memcpy(buf, self->curData, sz);
self->curSz -= sz;
self->curData += sz;
return sz;
} else if(self->isFinished) {
return AVERROR_EOF;
} else {
return AVERROR(EAGAIN);
}
}
/*######## Helper functions #########################################*/
std::unique_ptr<AVFormatContext> openOutfile (const char *ofname) {
// открыть поток для файла
AVFormatContext *fmtCtx=0;
int rc = avformat_alloc_output_context2(&fmtCtx, NULL, NULL, ofname);
if(rc<0) {
throw std::runtime_error("failed to crate context for output file: "s.append(av_err2str(rc)));
}
if (!(fmtCtx->oformat->flags & AVFMT_NOFILE)) {
rc = avio_open(&fmtCtx->pb, ofname, AVIO_FLAG_WRITE);
if (rc < 0) {
throw std::runtime_error("failed to open output file ("s.append(ofname)
.append("): ").append(av_err2str(rc)));
fprintf(stderr, "Could not open output file '%s'", ofname);
}
}
return std::unique_ptr<AVFormatContext>(fmtCtx);
};
/*######## main() example ###########################################*/
int main ( int argc, char *argv[] ) {
if(argc!=3) {
std::cerr << "Usage: " << argv [0] << " <infile> <outfile>" << std::endl;
return EXIT_FAILURE;
}
const char *ifname = argv[1];
const char *ofname = argv[2];
int rc, rv = EXIT_SUCCESS;
av_log_set_level(AV_LOG_TRACE);
AVInputFormat *fmt = av_find_input_format("alaw");
assert (fmt);
AVDictionary *inOpts = 0;
// Sets the sample rate of the output file.
av_dict_set(&inOpts, "sample_rate", "8000", 0);
// Sets the number of channels for the output file.
av_dict_set(&inOpts, "channels", "1", 0);
try {
constexpr size_t bufSz = 1024;
uint8_t buf[bufSz];
MuxRecorder sr(fmt, &inOpts);
std::shared_ptr<AVFormatContext> outCtx{openOutfile(ofname)};
sr.setOutput(outCtx);
av_dict_free (&inOpts);
rc = avformat_write_header(outCtx.get(), 0);
if(rc< 0) {
throw std::runtime_error("Failed to write output header: "s.append(av_err2str(rc)) );
}
std::ifstream is(ifname, std::ios::binary | std::ios::in);
if(!is) {
throw std::runtime_error("Failed to open input file: "s.append(strerror(errno)));
}
do {
is.read((char*)buf, bufSz);
sr.processData(buf, is.gcount());
} while (is);
sr.finalize();
rc = av_write_trailer(outCtx.get());
if(rc<0) {
throw std::runtime_error( "Failed to finalize output file: "s.append(av_err2str(rc)) );
}
} catch(std::exception &e) {
av_dict_free (&inOpts);
std::cerr << e.what() << std::endl;
rv = EXIT_FAILURE;
}
return rv;
}
// PS:
//
// I'm the author of this code snippet and hereby grant it to public domain.
// Я, автор данного отрывка кода, передаю его в общественное достояние.
Некоторые замечания:
- Данные на вход можно подавать произвольными фрагментами, у ffmpeg есть внутренняя буферезация, так что всё должно работать независимо от размера блока.
- Не особо отлаживал, так что возможны ошибки.
- Отдельно стоит проверить на утечки памяти.
- Не должно составить сложности упаковывать несколько дорожек в один контейнер (в том числе видео).
- В качестве поддерживаемого потока на вход
MuxRecorder'у должно быть возможно подать любой, поддерживаемый ffmpeg (с видео также не должно возникнуть проблем). - Если перекодирование всё же необходимо, то на данный пример можно накрутить вариант с ресампленгом, как в примере
transcode_aac.c/ответе @bbdd.
Полезные ссылки:
- Относительно хороший обзор последовательности обработки данных в libav: ffmpeg-libav-tutorial
- Пример чтения из памяти в ffmpeg:
avio_reading.c - Пример муксинга потоков:
remuxing.c
Благодаря комментарию пользователя Fat-Zer, было определено, что лучше всего использовать для определения кодека - av_guess_codec() функцию, которая в свою очередь, автоматически подбирает нужный идентификатор кодека.
Определённости ради, я не говорил, что так делать «лучше всего». Я просто обозначил, что так поступает ffmpeg, если пользователь не указал кодек. И можно использовать любой кодек на свой выбор. ИМХО, в своём полноценном приложении, этот выбор надо или дать пользователю, или сделать заранее самому, а не полагаться на то, как был собран ffmpeg.