Quiet capability probes, preserve runtime errors, and register the desktop portal application. Add logging and desktop launcher regression coverage.
773 lines
33 KiB
C
773 lines
33 KiB
C
// video.c — пробинг и транскодирование видео через FFmpeg (опционально, HAVE_FFMPEG)
|
|
#include "video.h"
|
|
#include "../media_async/media_async.h"
|
|
#include "../../lib/debug_config.h"
|
|
#include "../../lib/mem.h"
|
|
#include "../../lib/u_async.h"
|
|
#include "../../lib/platform_compat.h"
|
|
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <pthread.h>
|
|
#include <sys/stat.h>
|
|
|
|
#ifdef HAVE_FFMPEG
|
|
#include <libavformat/avformat.h>
|
|
#include <libavcodec/avcodec.h>
|
|
#include <libavcodec/bsf.h>
|
|
#include <libavutil/avutil.h>
|
|
#include <libavutil/imgutils.h>
|
|
#include <libavutil/opt.h>
|
|
#include <libavutil/channel_layout.h>
|
|
#include <libavutil/samplefmt.h>
|
|
#include <libswscale/swscale.h>
|
|
#include <libswresample/swresample.h>
|
|
#endif
|
|
|
|
/* ─── прогресс активного транскода (атомарно, читается из GUI-потока) ─── */
|
|
|
|
static int g_progress_permille = -1; /* -1 = транскода нет, иначе 0..1000 */
|
|
static int g_transcode_active = 0;
|
|
|
|
static int vc_progress_get(void) { return __atomic_load_n(&g_progress_permille, __ATOMIC_RELAXED); }
|
|
static void vc_progress_set(int v) { __atomic_store_n(&g_progress_permille, v, __ATOMIC_RELAXED); }
|
|
static int vc_active_get(void) { return __atomic_load_n(&g_transcode_active, __ATOMIC_RELAXED); }
|
|
static void vc_active_set(int v) { __atomic_store_n(&g_transcode_active, v, __ATOMIC_RELAXED); }
|
|
|
|
int video_transcode_get_progress(void) { return vc_progress_get(); }
|
|
int video_transcode_is_active(void) { return vc_active_get(); }
|
|
|
|
/* Пробы не меняют глобальный уровень: параллельные звонки сохраняют рабочие ошибки. */
|
|
#ifdef _MSC_VER
|
|
static __declspec(thread) struct { unsigned depth, repeated; char last[1024]; } g_codec_probe;
|
|
#else
|
|
static __thread struct { unsigned depth, repeated; char last[1024]; } g_codec_probe;
|
|
#endif
|
|
|
|
static void video_codec_probe_flush(void) {
|
|
if (g_codec_probe.repeated)
|
|
DEBUG_DEBUG(DEBUG_CATEGORY_VIDEO, "ffmpeg probe: repeated=%u message=%s", g_codec_probe.repeated, g_codec_probe.last);
|
|
g_codec_probe.repeated = 0;
|
|
}
|
|
|
|
void video_codec_probe_begin(void) {
|
|
video_ffmpeg_init();
|
|
++g_codec_probe.depth;
|
|
}
|
|
|
|
void video_codec_probe_end(void) {
|
|
if (!g_codec_probe.depth) { DEBUG_ERROR(DEBUG_CATEGORY_VIDEO, "video codec probe end without begin"); return; }
|
|
if (--g_codec_probe.depth) return;
|
|
video_codec_probe_flush(); g_codec_probe.last[0] = '\0';
|
|
}
|
|
|
|
int video_codec_log_level(int level) {
|
|
return g_codec_probe.depth && level >= DEBUG_LEVEL_ERROR && level < DEBUG_LEVEL_DEBUG ? DEBUG_LEVEL_DEBUG : level;
|
|
}
|
|
|
|
#ifndef HAVE_FFMPEG
|
|
|
|
/* ─── сборка без ffmpeg: стабы ─── */
|
|
|
|
void video_ffmpeg_init(void) {}
|
|
|
|
int video_probe(const char* path) { (void)path; return 0; }
|
|
int video_get_info(const char* path, struct video_info* out) { (void)path; (void)out; return -1; }
|
|
int video_needs_transcode(const struct video_info* info) { (void)info; return 1; }
|
|
|
|
int video_transcode_start(struct media_async* owner, struct UASYNC* ua, const char* src, const char* dst,
|
|
void (*done)(void* arg, int err, const struct video_info* info), void* arg) {
|
|
(void)owner; (void)ua; (void)src; (void)dst; (void)done; (void)arg;
|
|
DEBUG_ERROR(DEBUG_CATEGORY_VIDEO, "video: built without FFmpeg (HAVE_FFMPEG undefined)");
|
|
return -1;
|
|
}
|
|
|
|
int video_extract_thumbnail(const char* src, const char* jpg_out, int max_w) {
|
|
(void)src; (void)jpg_out; (void)max_w;
|
|
DEBUG_ERROR(DEBUG_CATEGORY_VIDEO, "video: built without FFmpeg (HAVE_FFMPEG undefined)");
|
|
return -1;
|
|
}
|
|
|
|
#else /* HAVE_FFMPEG */
|
|
|
|
/* ─── одноразовая инициализация ffmpeg ─── */
|
|
|
|
static pthread_once_t g_ffmpeg_once = PTHREAD_ONCE_INIT;
|
|
|
|
/* Пропускаем диагностику AMF и существенные поля trace_headers, остальные подробности FFmpeg отсекаем. */
|
|
static void video_av_log_cb(void* avcl, int level, const char* fmt, va_list vl) {
|
|
const char *encoder = NULL;
|
|
int headers = 0;
|
|
if (level > AV_LOG_WARNING) {
|
|
if (!debug_should_output(DEBUG_LEVEL_TRACE, DEBUG_CATEGORY_DEBUG)) return;
|
|
if (!avcl) return;
|
|
if (*(const AVClass **)avcl == av_bsf_get_class()) {
|
|
const AVBSFContext *filter = avcl;
|
|
if (!filter->filter || strcmp(filter->filter->name, "trace_headers")) return;
|
|
headers = 1;
|
|
} else {
|
|
if (*(const AVClass **)avcl != avcodec_get_class()) return;
|
|
const AVCodecContext *codec = avcl;
|
|
if (!codec->codec || !codec->codec->name) return;
|
|
const char *name = codec->codec->name;
|
|
size_t length = strlen(name);
|
|
if (length < 4 || strcmp(name + length - 4, "_amf")) return;
|
|
encoder = name;
|
|
}
|
|
}
|
|
char buf[1024];
|
|
vsnprintf(buf, sizeof(buf), fmt, vl);
|
|
size_t len = strlen(buf);
|
|
while (len > 0 && (buf[len - 1] == '\n' || buf[len - 1] == '\r')) buf[--len] = '\0';
|
|
if (!len) return;
|
|
if (g_codec_probe.depth && level <= AV_LOG_WARNING) {
|
|
if (!strcmp(g_codec_probe.last, buf)) { ++g_codec_probe.repeated; return; }
|
|
video_codec_probe_flush(); snprintf(g_codec_probe.last, sizeof(g_codec_probe.last), "%s", buf);
|
|
DEBUG_DEBUG(DEBUG_CATEGORY_VIDEO, "ffmpeg probe source=%s level=%d: %s", avcl ? av_default_item_name(avcl) : "none", level, buf);
|
|
return;
|
|
}
|
|
if (headers) {
|
|
if (!strstr(buf, "Packet:") && !strstr(buf, "init_qp_minus26") && !strstr(buf, "slice_qp_delta")
|
|
&& !strstr(buf, "pic_width_in_luma_samples") && !strstr(buf, "pic_height_in_luma_samples")
|
|
&& !strstr(buf, "pic_parameter_set_id") && !strstr(buf, "cu_qp_delta_enabled_flag")
|
|
&& !strstr(buf, "diff_cu_qp_delta_depth") && !strstr(buf, "slice_type")) return;
|
|
DEBUG_TRACE(DEBUG_CATEGORY_DEBUG, "ffmpeg headers: %s", buf);
|
|
} else if (encoder) {
|
|
if (!strstr(buf, "Warning:") && !strstr(buf, "Error:")
|
|
&& (strstr(buf, "::SubmitInput() : format") || strstr(buf, "SubmitToEncoder() End: frame pts=")
|
|
|| strstr(buf, "::QueryOutput()") || strstr(buf, "QueryOutput() End: frame pts="))) return;
|
|
DEBUG_TRACE(DEBUG_CATEGORY_DEBUG, "ffmpeg AMF encoder=%s: %s", encoder, buf);
|
|
} else if (level <= AV_LOG_ERROR) DEBUG_ERROR(DEBUG_CATEGORY_VIDEO, "ffmpeg: %s", buf);
|
|
else DEBUG_WARN(DEBUG_CATEGORY_VIDEO, "ffmpeg: %s", buf);
|
|
}
|
|
|
|
static void ffmpeg_init_once(void) {
|
|
avformat_network_init();
|
|
}
|
|
|
|
void video_ffmpeg_init(void) {
|
|
pthread_once(&g_ffmpeg_once, ffmpeg_init_once);
|
|
/* Qt Multimedia заменяет глобальный callback при загрузке backend; возвращаем общий лог перед работой с codec. */
|
|
av_log_set_callback(video_av_log_cb);
|
|
av_log_set_level(AV_LOG_DEBUG);
|
|
}
|
|
|
|
/* строковое описание кода ошибки ffmpeg (используется только в worker-потоке) */
|
|
static const char* vc_errstr(int err) {
|
|
static char buf[64];
|
|
av_strerror(err, buf, sizeof(buf));
|
|
return buf;
|
|
}
|
|
|
|
/* ─── пробинг ─── */
|
|
|
|
int video_probe(const char* path) {
|
|
if (!path || !path[0]) return 0;
|
|
video_ffmpeg_init();
|
|
|
|
AVFormatContext* fmt = NULL;
|
|
if (avformat_open_input(&fmt, path, NULL, NULL) != 0) return 0;
|
|
|
|
if (avformat_find_stream_info(fmt, NULL) < 0) {
|
|
avformat_close_input(&fmt);
|
|
return 0;
|
|
}
|
|
|
|
int is_video = 0;
|
|
for (unsigned i = 0; i < fmt->nb_streams; i++) {
|
|
if (fmt->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) { is_video = 1; break; }
|
|
}
|
|
avformat_close_input(&fmt);
|
|
DEBUG_DEBUG(DEBUG_CATEGORY_VIDEO, "video: probe %s = %d", path, is_video);
|
|
return is_video;
|
|
}
|
|
|
|
int video_get_info(const char* path, struct video_info* out) {
|
|
if (!out) return -1;
|
|
memset(out, 0, sizeof(*out));
|
|
if (!path || !path[0]) return -1;
|
|
video_ffmpeg_init();
|
|
|
|
AVFormatContext* fmt = NULL;
|
|
if (avformat_open_input(&fmt, path, NULL, NULL) != 0) {
|
|
DEBUG_ERROR(DEBUG_CATEGORY_VIDEO, "video: open failed: %s", path);
|
|
return -1;
|
|
}
|
|
if (avformat_find_stream_info(fmt, NULL) < 0) {
|
|
DEBUG_WARN(DEBUG_CATEGORY_VIDEO, "video: find_stream_info failed: %s", path);
|
|
avformat_close_input(&fmt);
|
|
return -1;
|
|
}
|
|
|
|
AVStream* vst = NULL;
|
|
for (unsigned i = 0; i < fmt->nb_streams; i++) {
|
|
AVCodecParameters* p = fmt->streams[i]->codecpar;
|
|
if (p->codec_type == AVMEDIA_TYPE_VIDEO && !vst) { vst = fmt->streams[i]; }
|
|
else if (p->codec_type == AVMEDIA_TYPE_AUDIO) { out->has_audio = 1; }
|
|
}
|
|
|
|
if (!vst) {
|
|
DEBUG_WARN(DEBUG_CATEGORY_VIDEO, "video: no video stream: %s", path);
|
|
avformat_close_input(&fmt);
|
|
return -1;
|
|
}
|
|
|
|
out->is_video = 1;
|
|
out->width = vst->codecpar->width;
|
|
out->height = vst->codecpar->height;
|
|
out->bitrate = vst->codecpar->bit_rate ? vst->codecpar->bit_rate : fmt->bit_rate;
|
|
if (fmt->duration != AV_NOPTS_VALUE) out->duration_ms = fmt->duration / 1000;
|
|
else if (vst->duration != AV_NOPTS_VALUE && vst->time_base.num > 0)
|
|
out->duration_ms = av_rescale_q(vst->duration, vst->time_base, (AVRational){1, 1000});
|
|
|
|
const AVCodecDescriptor* desc = avcodec_descriptor_get(vst->codecpar->codec_id);
|
|
if (desc) snprintf(out->codec, sizeof(out->codec), "%s", desc->name);
|
|
if (fmt->iformat && fmt->iformat->name) snprintf(out->container, sizeof(out->container), "%s", fmt->iformat->name);
|
|
|
|
avformat_close_input(&fmt);
|
|
|
|
DEBUG_INFO(DEBUG_CATEGORY_VIDEO,
|
|
"video: info %s: %dx%d %dkbit codec=%s container=%s dur=%lldms audio=%d",
|
|
path, out->width, out->height, (int)(out->bitrate / 1000),
|
|
out->codec, out->container, (long long)out->duration_ms, out->has_audio);
|
|
return 0;
|
|
}
|
|
|
|
int video_needs_transcode(const struct video_info* info) {
|
|
if (!info || !info->is_video) return 1;
|
|
if (!strstr(info->container, "mp4") && !strstr(info->container, "mov")) return 1;
|
|
if (strcmp(info->codec, "h264") != 0) return 1;
|
|
if (info->width > VIDEO_TARGET_MAX_WIDTH || info->height > VIDEO_TARGET_MAX_HEIGHT) return 1;
|
|
if (info->bitrate > VIDEO_TARGET_MAX_BITRATE) return 1;
|
|
return 0;
|
|
}
|
|
|
|
/* ─── транскодирование ─── */
|
|
|
|
struct vc_stream {
|
|
int in_idx;
|
|
AVRational in_time_base; /* time_base входного потока (для рескейла PTS) */
|
|
AVCodecContext* dec_ctx;
|
|
AVCodecContext* enc_ctx;
|
|
AVStream* out_stream;
|
|
struct SwsContext* sws;
|
|
struct SwrContext* swr;
|
|
AVFrame* frame;
|
|
int64_t next_pts; /* аудио: счётчик в сэмплах (ед. time_base энкодера) */
|
|
};
|
|
|
|
struct vc_ctx {
|
|
char src[1024];
|
|
char dst[1024];
|
|
struct UASYNC* ua;
|
|
void (*done)(void* arg, int err, const struct video_info* info);
|
|
struct video_info result;
|
|
void* arg;
|
|
|
|
int err;
|
|
int64_t src_size;
|
|
|
|
AVFormatContext* ifmt;
|
|
AVFormatContext* ofmt;
|
|
struct vc_stream vid;
|
|
struct vc_stream aud;
|
|
};
|
|
|
|
static int vc_copy_file(const char* src, const char* dst) {
|
|
FILE* s = fopen(src, "rb");
|
|
if (!s) return -1;
|
|
FILE* d = fopen(dst, "wb");
|
|
if (!d) { fclose(s); return -1; }
|
|
uint8_t buf[65536];
|
|
size_t rd;
|
|
while ((rd = fread(buf, 1, sizeof(buf), s)) > 0) {
|
|
if (fwrite(buf, 1, rd, d) != rd) { fclose(s); fclose(d); return -1; }
|
|
}
|
|
int rc = ferror(s) ? -1 : 0;
|
|
fclose(s);
|
|
if (fclose(d) != 0) rc = -1;
|
|
return rc;
|
|
}
|
|
|
|
static int vc_open_decoder(struct vc_stream* st, AVFormatContext* ifmt, enum AVMediaType type) {
|
|
const char* tname = (type == AVMEDIA_TYPE_VIDEO) ? "video" : "audio";
|
|
int idx = av_find_best_stream(ifmt, type, -1, -1, NULL, 0);
|
|
if (idx < 0) return -1;
|
|
|
|
AVCodecParameters* par = ifmt->streams[idx]->codecpar;
|
|
const AVCodec* dec = avcodec_find_decoder(par->codec_id);
|
|
if (!dec) { DEBUG_ERROR(DEBUG_CATEGORY_VIDEO, "video: no decoder for %s stream %d (codec_id=%d)", tname, idx, par->codec_id); return -1; }
|
|
|
|
st->dec_ctx = avcodec_alloc_context3(dec);
|
|
if (!st->dec_ctx) { DEBUG_ERROR(DEBUG_CATEGORY_VIDEO, "video: alloc decoder ctx failed (%s)", tname); return -1; }
|
|
if (avcodec_parameters_to_context(st->dec_ctx, par) < 0) {
|
|
DEBUG_ERROR(DEBUG_CATEGORY_VIDEO, "video: parameters_to_context failed (%s)", tname); return -1;
|
|
}
|
|
if (avcodec_open2(st->dec_ctx, dec, NULL) < 0) {
|
|
DEBUG_ERROR(DEBUG_CATEGORY_VIDEO, "video: open decoder failed (%s %s)", tname, dec->name); return -1;
|
|
}
|
|
st->in_idx = idx;
|
|
st->in_time_base = ifmt->streams[idx]->time_base;
|
|
st->frame = av_frame_alloc();
|
|
if (!st->frame) { DEBUG_ERROR(DEBUG_CATEGORY_VIDEO, "video: av_frame_alloc failed (%s)", tname); return -1; }
|
|
DEBUG_INFO(DEBUG_CATEGORY_VIDEO, "video: decoder %s stream %d: %s", tname, idx, dec->name);
|
|
return 0;
|
|
}
|
|
|
|
static void vc_stream_close(struct vc_stream* st) {
|
|
if (st->sws) { sws_freeContext(st->sws); st->sws = NULL; }
|
|
if (st->swr) { swr_free(&st->swr); st->swr = NULL; }
|
|
if (st->frame){ av_frame_free(&st->frame); st->frame = NULL; }
|
|
if (st->dec_ctx) { avcodec_free_context(&st->dec_ctx); st->dec_ctx = NULL; }
|
|
if (st->enc_ctx) { avcodec_free_context(&st->enc_ctx); st->enc_ctx = NULL; }
|
|
}
|
|
|
|
static int vc_open_video(struct vc_ctx* ctx) {
|
|
struct vc_stream* st = &ctx->vid;
|
|
AVCodecContext* dec = st->dec_ctx;
|
|
|
|
int dst_w = dec->width, dst_h = dec->height;
|
|
double f = 1.0;
|
|
if (dst_w > VIDEO_TARGET_MAX_WIDTH) f = (double)VIDEO_TARGET_MAX_WIDTH / dst_w;
|
|
if (dst_h * f > VIDEO_TARGET_MAX_HEIGHT) f = (double)VIDEO_TARGET_MAX_HEIGHT / dst_h;
|
|
dst_w = (int)(dec->width * f + 0.5);
|
|
dst_h = (int)(dec->height * f + 0.5);
|
|
dst_w &= ~1; if (dst_w < 2) dst_w = 2;
|
|
dst_h &= ~1; if (dst_h < 2) dst_h = 2;
|
|
|
|
const AVCodec* enc = avcodec_find_encoder_by_name("libx264");
|
|
if (!enc) enc = avcodec_find_encoder(AV_CODEC_ID_H264);
|
|
if (!enc) { DEBUG_ERROR(DEBUG_CATEGORY_VIDEO, "video: no H.264 encoder"); return -1; }
|
|
|
|
st->enc_ctx = avcodec_alloc_context3(enc);
|
|
if (!st->enc_ctx) { DEBUG_ERROR(DEBUG_CATEGORY_VIDEO, "video: alloc encoder ctx failed"); return -1; }
|
|
st->enc_ctx->width = dst_w;
|
|
st->enc_ctx->height = dst_h;
|
|
st->enc_ctx->pix_fmt = AV_PIX_FMT_YUV420P;
|
|
st->enc_ctx->time_base = (AVRational){1, 1000};
|
|
st->enc_ctx->framerate = av_guess_frame_rate(ctx->ifmt, ctx->ifmt->streams[st->in_idx], NULL);
|
|
st->enc_ctx->gop_size = 12 * ((st->enc_ctx->framerate.num + st->enc_ctx->framerate.den - 1) / st->enc_ctx->framerate.den);
|
|
if (st->enc_ctx->gop_size < 24) st->enc_ctx->gop_size = 24;
|
|
st->enc_ctx->max_b_frames = 0;
|
|
st->enc_ctx->bit_rate = VIDEO_TARGET_MAX_BITRATE;
|
|
st->enc_ctx->rc_max_rate = VIDEO_TARGET_MAX_BITRATE;
|
|
st->enc_ctx->rc_buffer_size = 2 * VIDEO_TARGET_MAX_BITRATE;
|
|
|
|
AVDictionary* opts = NULL;
|
|
av_dict_set(&opts, "preset", "medium", 0);
|
|
av_dict_set(&opts, "crf", "23", 0);
|
|
if (avcodec_open2(st->enc_ctx, enc, &opts) < 0) {
|
|
av_dict_free(&opts);
|
|
DEBUG_ERROR(DEBUG_CATEGORY_VIDEO, "video: open video encoder failed (%s)", enc->name);
|
|
return -1;
|
|
}
|
|
av_dict_free(&opts);
|
|
|
|
st->out_stream = avformat_new_stream(ctx->ofmt, NULL);
|
|
if (!st->out_stream) { DEBUG_ERROR(DEBUG_CATEGORY_VIDEO, "video: avformat_new_stream failed (video)"); return -1; }
|
|
if (avcodec_parameters_from_context(st->out_stream->codecpar, st->enc_ctx) < 0) {
|
|
DEBUG_ERROR(DEBUG_CATEGORY_VIDEO, "video: parameters_from_context failed (video)"); return -1;
|
|
}
|
|
st->out_stream->time_base = st->enc_ctx->time_base;
|
|
st->out_stream->avg_frame_rate = st->enc_ctx->framerate;
|
|
|
|
st->sws = sws_getContext(dec->width, dec->height, dec->pix_fmt,
|
|
dst_w, dst_h, AV_PIX_FMT_YUV420P,
|
|
SWS_BILINEAR, NULL, NULL, NULL);
|
|
if (!st->sws) { DEBUG_ERROR(DEBUG_CATEGORY_VIDEO, "video: sws_getContext failed"); return -1; }
|
|
|
|
DEBUG_INFO(DEBUG_CATEGORY_VIDEO, "video: transcode video %dx%d -> %dx%d h264 2Mbit",
|
|
dec->width, dec->height, dst_w, dst_h);
|
|
return 0;
|
|
}
|
|
|
|
static int vc_open_audio(struct vc_ctx* ctx) {
|
|
struct vc_stream* st = &ctx->aud;
|
|
AVCodecContext* dec = st->dec_ctx;
|
|
|
|
const AVCodec* enc = avcodec_find_encoder(AV_CODEC_ID_AAC);
|
|
if (!enc) { DEBUG_WARN(DEBUG_CATEGORY_VIDEO, "video: no AAC encoder, dropping audio"); return -1; }
|
|
|
|
st->enc_ctx = avcodec_alloc_context3(enc);
|
|
if (!st->enc_ctx) { DEBUG_ERROR(DEBUG_CATEGORY_VIDEO, "video: alloc audio encoder ctx failed"); return -1; }
|
|
st->enc_ctx->bit_rate = VIDEO_TARGET_AUDIO_BITRATE;
|
|
st->enc_ctx->sample_rate = dec->sample_rate ? dec->sample_rate : 44100;
|
|
st->enc_ctx->sample_fmt = AV_SAMPLE_FMT_FLTP;
|
|
if (av_channel_layout_copy(&st->enc_ctx->ch_layout, &dec->ch_layout) < 0) {
|
|
DEBUG_ERROR(DEBUG_CATEGORY_VIDEO, "video: ch_layout copy failed"); return -1;
|
|
}
|
|
st->enc_ctx->time_base = (AVRational){1, st->enc_ctx->sample_rate};
|
|
|
|
if (avcodec_open2(st->enc_ctx, enc, NULL) < 0) {
|
|
DEBUG_ERROR(DEBUG_CATEGORY_VIDEO, "video: open AAC encoder failed"); return -1;
|
|
}
|
|
|
|
st->out_stream = avformat_new_stream(ctx->ofmt, NULL);
|
|
if (!st->out_stream) { DEBUG_ERROR(DEBUG_CATEGORY_VIDEO, "video: avformat_new_stream failed (audio)"); return -1; }
|
|
if (avcodec_parameters_from_context(st->out_stream->codecpar, st->enc_ctx) < 0) {
|
|
DEBUG_ERROR(DEBUG_CATEGORY_VIDEO, "video: parameters_from_context failed (audio)"); return -1;
|
|
}
|
|
st->out_stream->time_base = st->enc_ctx->time_base;
|
|
|
|
if (dec->sample_fmt != st->enc_ctx->sample_fmt ||
|
|
dec->sample_rate != st->enc_ctx->sample_rate ||
|
|
av_channel_layout_compare(&dec->ch_layout, &st->enc_ctx->ch_layout)) {
|
|
int rc = swr_alloc_set_opts2(&st->swr, &st->enc_ctx->ch_layout, st->enc_ctx->sample_fmt,
|
|
st->enc_ctx->sample_rate, &dec->ch_layout, dec->sample_fmt,
|
|
dec->sample_rate, 0, NULL);
|
|
if (rc < 0 || swr_init(st->swr) < 0) { DEBUG_ERROR(DEBUG_CATEGORY_VIDEO, "video: swr init failed"); return -1; }
|
|
}
|
|
|
|
DEBUG_INFO(DEBUG_CATEGORY_VIDEO, "video: transcode audio -> AAC 128k %dHz", st->enc_ctx->sample_rate);
|
|
return 0;
|
|
}
|
|
|
|
static int vc_encode(struct vc_ctx* ctx, struct vc_stream* st, int flush) {
|
|
AVCodecContext* enc = st->enc_ctx;
|
|
const char* tname = (enc->codec_type == AVMEDIA_TYPE_VIDEO) ? "video" : "audio";
|
|
while (1) {
|
|
AVPacket* pkt = av_packet_alloc();
|
|
if (!pkt) { DEBUG_ERROR(DEBUG_CATEGORY_VIDEO, "video: av_packet_alloc failed (%s)", tname); return -1; }
|
|
int ret = avcodec_receive_packet(enc, pkt);
|
|
if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) {
|
|
av_packet_free(&pkt);
|
|
if (flush && ret == AVERROR(EAGAIN)) continue;
|
|
return 0;
|
|
}
|
|
if (ret < 0) { av_packet_free(&pkt); DEBUG_ERROR(DEBUG_CATEGORY_VIDEO, "video: receive_packet failed (%s): %s", tname, vc_errstr(ret)); return -1; }
|
|
|
|
av_packet_rescale_ts(pkt, enc->time_base, st->out_stream->time_base);
|
|
pkt->stream_index = st->out_stream->index;
|
|
int rc = av_interleaved_write_frame(ctx->ofmt, pkt);
|
|
av_packet_free(&pkt);
|
|
if (rc < 0) { DEBUG_ERROR(DEBUG_CATEGORY_VIDEO, "video: write_frame failed (%s): %s", tname, vc_errstr(rc)); return -1; }
|
|
}
|
|
}
|
|
|
|
static int vc_decode_send(struct vc_ctx* ctx, struct vc_stream* st, AVPacket* pkt, int* eof) {
|
|
const char* tname = (st->enc_ctx->codec_type == AVMEDIA_TYPE_VIDEO) ? "video" : "audio";
|
|
int ret = avcodec_send_packet(st->dec_ctx, pkt);
|
|
if (ret < 0 && ret != AVERROR(EAGAIN)) {
|
|
DEBUG_ERROR(DEBUG_CATEGORY_VIDEO, "video: send_packet failed (%s): %s", tname, vc_errstr(ret)); return -1;
|
|
}
|
|
if (ret == AVERROR_EOF) { *eof = 1; }
|
|
|
|
while (1) {
|
|
ret = avcodec_receive_frame(st->dec_ctx, st->frame);
|
|
if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) break;
|
|
if (ret < 0) { DEBUG_ERROR(DEBUG_CATEGORY_VIDEO, "video: receive_frame failed (%s): %s", tname, vc_errstr(ret)); return -1; }
|
|
|
|
AVFrame* src_frame = st->frame;
|
|
AVFrame* out_frame = src_frame;
|
|
AVFrame* tmp = NULL;
|
|
|
|
if (st->enc_ctx->codec_type == AVMEDIA_TYPE_VIDEO) {
|
|
if (src_frame->width != st->enc_ctx->width || src_frame->height != st->enc_ctx->height ||
|
|
src_frame->format != st->enc_ctx->pix_fmt) {
|
|
tmp = av_frame_alloc();
|
|
if (!tmp) { DEBUG_ERROR(DEBUG_CATEGORY_VIDEO, "video: av_frame_alloc failed (scale)"); return -1; }
|
|
tmp->format = st->enc_ctx->pix_fmt;
|
|
tmp->width = st->enc_ctx->width;
|
|
tmp->height = st->enc_ctx->height;
|
|
if (av_frame_get_buffer(tmp, 0) < 0) { av_frame_free(&tmp); DEBUG_ERROR(DEBUG_CATEGORY_VIDEO, "video: frame_get_buffer failed (scale)"); return -1; }
|
|
sws_scale(st->sws, (const uint8_t* const*)src_frame->data, src_frame->linesize,
|
|
0, src_frame->height, tmp->data, tmp->linesize);
|
|
tmp->pts = src_frame->pts;
|
|
out_frame = tmp;
|
|
}
|
|
} else if (st->enc_ctx->codec_type == AVMEDIA_TYPE_AUDIO) {
|
|
if (st->swr) {
|
|
tmp = av_frame_alloc();
|
|
if (!tmp) { DEBUG_ERROR(DEBUG_CATEGORY_VIDEO, "video: av_frame_alloc failed (resample)"); return -1; }
|
|
tmp->format = st->enc_ctx->sample_fmt;
|
|
av_channel_layout_copy(&tmp->ch_layout, &st->enc_ctx->ch_layout);
|
|
tmp->sample_rate = st->enc_ctx->sample_rate;
|
|
tmp->nb_samples = av_rescale_rnd(swr_get_delay(st->swr, src_frame->sample_rate) + src_frame->nb_samples,
|
|
st->enc_ctx->sample_rate, src_frame->sample_rate, AV_ROUND_UP);
|
|
if (av_frame_get_buffer(tmp, 0) < 0) { av_frame_free(&tmp); DEBUG_ERROR(DEBUG_CATEGORY_VIDEO, "video: frame_get_buffer failed (resample)"); return -1; }
|
|
if (swr_convert_frame(st->swr, tmp, src_frame) < 0) { av_frame_free(&tmp); DEBUG_ERROR(DEBUG_CATEGORY_VIDEO, "video: swr_convert_frame failed"); return -1; }
|
|
tmp->pts = st->next_pts;
|
|
st->next_pts += tmp->nb_samples;
|
|
out_frame = tmp;
|
|
}
|
|
}
|
|
|
|
out_frame->pts = (st->enc_ctx->codec_type == AVMEDIA_TYPE_VIDEO)
|
|
? av_rescale_q_rnd(out_frame->pts, st->in_time_base, st->enc_ctx->time_base,
|
|
AV_ROUND_NEAR_INF | AV_ROUND_PASS_MINMAX)
|
|
: out_frame->pts; /* аудио: pts уже в сэмплах time_base энкодера */
|
|
if (avcodec_send_frame(st->enc_ctx, out_frame) < 0) {
|
|
if (tmp) av_frame_free(&tmp);
|
|
DEBUG_ERROR(DEBUG_CATEGORY_VIDEO, "video: send_frame failed (%s)", tname);
|
|
return -1;
|
|
}
|
|
if (tmp) av_frame_free(&tmp);
|
|
|
|
if (vc_encode(ctx, st, 0) < 0) return -1;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static void vc_flush(struct vc_ctx* ctx, struct vc_stream* st) {
|
|
avcodec_send_frame(st->enc_ctx, NULL);
|
|
vc_encode(ctx, st, 1);
|
|
}
|
|
|
|
static int vc_run_transcode(struct vc_ctx* ctx) {
|
|
video_ffmpeg_init();
|
|
|
|
int ret = avformat_open_input(&ctx->ifmt, ctx->src, NULL, NULL);
|
|
if (ret != 0) { DEBUG_ERROR(DEBUG_CATEGORY_VIDEO, "video: avformat_open_input failed: %s", vc_errstr(ret)); return -1; }
|
|
if (avformat_find_stream_info(ctx->ifmt, NULL) < 0) {
|
|
DEBUG_ERROR(DEBUG_CATEGORY_VIDEO, "video: avformat_find_stream_info failed: %s", ctx->src); return -1;
|
|
}
|
|
|
|
if (avformat_alloc_output_context2(&ctx->ofmt, NULL, "mp4", ctx->dst) < 0) {
|
|
DEBUG_ERROR(DEBUG_CATEGORY_VIDEO, "video: alloc output context (mp4) failed: %s", ctx->dst); return -1;
|
|
}
|
|
|
|
if (vc_open_decoder(&ctx->vid, ctx->ifmt, AVMEDIA_TYPE_VIDEO) < 0) return -1;
|
|
if (vc_open_video(ctx) < 0) return -1;
|
|
if (vc_open_decoder(&ctx->aud, ctx->ifmt, AVMEDIA_TYPE_AUDIO) == 0)
|
|
vc_open_audio(ctx);
|
|
|
|
if (!(ctx->ofmt->oformat->flags & AVFMT_NOFILE)) {
|
|
ret = avio_open(&ctx->ofmt->pb, ctx->dst, AVIO_FLAG_WRITE);
|
|
if (ret < 0) { DEBUG_ERROR(DEBUG_CATEGORY_VIDEO, "video: avio_open failed: %s", vc_errstr(ret)); return -1; }
|
|
}
|
|
if (avformat_write_header(ctx->ofmt, NULL) < 0) {
|
|
DEBUG_ERROR(DEBUG_CATEGORY_VIDEO, "video: avformat_write_header failed: %s", ctx->dst); return -1;
|
|
}
|
|
|
|
int64_t bytes_done = 0;
|
|
AVPacket* pkt = av_packet_alloc();
|
|
if (!pkt) { DEBUG_ERROR(DEBUG_CATEGORY_VIDEO, "video: av_packet_alloc failed"); return -1; }
|
|
|
|
while (1) {
|
|
ret = av_read_frame(ctx->ifmt, pkt);
|
|
if (ret == AVERROR_EOF) break;
|
|
if (ret < 0) {
|
|
DEBUG_ERROR(DEBUG_CATEGORY_VIDEO, "video: av_read_frame failed: %s", vc_errstr(ret));
|
|
av_packet_free(&pkt);
|
|
return -1;
|
|
}
|
|
|
|
if (ctx->src_size > 0) {
|
|
bytes_done += pkt->size;
|
|
int p = (int)(bytes_done * 1000 / ctx->src_size);
|
|
if (p > 1000) p = 1000;
|
|
vc_progress_set(p);
|
|
}
|
|
|
|
if (pkt->stream_index == ctx->vid.in_idx) {
|
|
int eof = 0;
|
|
if (vc_decode_send(ctx, &ctx->vid, pkt, &eof) < 0) { av_packet_free(&pkt); return -1; }
|
|
} else if (ctx->aud.in_idx >= 0 && pkt->stream_index == ctx->aud.in_idx && ctx->aud.enc_ctx) {
|
|
int eof = 0;
|
|
if (vc_decode_send(ctx, &ctx->aud, pkt, &eof) < 0) { av_packet_free(&pkt); return -1; }
|
|
}
|
|
av_packet_unref(pkt);
|
|
}
|
|
av_packet_free(&pkt);
|
|
|
|
/* флашим декодеры (досылаем NULL → выкачиваем оставшиеся кадры), затем энкодеры */
|
|
{ int eof = 0; vc_decode_send(ctx, &ctx->vid, NULL, &eof); }
|
|
if (ctx->aud.enc_ctx) { int eof = 0; vc_decode_send(ctx, &ctx->aud, NULL, &eof); }
|
|
vc_flush(ctx, &ctx->vid);
|
|
if (ctx->aud.enc_ctx) vc_flush(ctx, &ctx->aud);
|
|
|
|
if (av_write_trailer(ctx->ofmt) < 0) {
|
|
DEBUG_ERROR(DEBUG_CATEGORY_VIDEO, "video: av_write_trailer failed: %s", ctx->dst); return -1;
|
|
}
|
|
vc_progress_set(1000);
|
|
DEBUG_INFO(DEBUG_CATEGORY_VIDEO, "video: wrote %lld bytes -> %s", (long long)bytes_done, ctx->dst);
|
|
return 0;
|
|
}
|
|
|
|
static void vc_cleanup(struct vc_ctx* ctx) {
|
|
vc_stream_close(&ctx->vid);
|
|
vc_stream_close(&ctx->aud);
|
|
if (ctx->ofmt) {
|
|
if (ctx->ofmt->pb) avio_closep(&ctx->ofmt->pb);
|
|
avformat_free_context(ctx->ofmt);
|
|
ctx->ofmt = NULL;
|
|
}
|
|
if (ctx->ifmt) { avformat_close_input(&ctx->ifmt); ctx->ifmt = NULL; }
|
|
}
|
|
|
|
static void vc_done_trampoline(void* raw, int error) {
|
|
struct vc_ctx* ctx = (struct vc_ctx*)raw;
|
|
vc_active_set(0);
|
|
vc_progress_set(-1);
|
|
if (ctx->done) ctx->done(ctx->arg, error ? error : ctx->err, &ctx->result);
|
|
u_free(ctx);
|
|
}
|
|
|
|
static void vc_thread_entry(void* raw) {
|
|
struct vc_ctx* ctx = (struct vc_ctx*)raw;
|
|
|
|
struct video_info info;
|
|
if (video_get_info(ctx->src, &info) != 0 || !info.is_video) {
|
|
DEBUG_ERROR(DEBUG_CATEGORY_VIDEO, "video: not a video file: %s", ctx->src);
|
|
ctx->err = -1;
|
|
goto out;
|
|
}
|
|
|
|
if (!video_needs_transcode(&info)) {
|
|
DEBUG_INFO(DEBUG_CATEGORY_VIDEO, "video: no transcode needed (mp4/h264 %dx%d %dkbit <= limits), copy as-is: %s",
|
|
info.width, info.height, (int)(info.bitrate / 1000), ctx->src);
|
|
vc_progress_set(0);
|
|
ctx->err = vc_copy_file(ctx->src, ctx->dst) == 0 ? 0 : -2;
|
|
if (ctx->err) DEBUG_ERROR(DEBUG_CATEGORY_VIDEO, "video: copy failed %s -> %s", ctx->src, ctx->dst);
|
|
vc_progress_set(ctx->err ? -1 : 1000);
|
|
goto out;
|
|
}
|
|
|
|
DEBUG_INFO(DEBUG_CATEGORY_VIDEO, "video: transcode %s -> %s (src %dx%d %dkbit %s/%s, size=%lld)",
|
|
ctx->src, ctx->dst, info.width, info.height, (int)(info.bitrate / 1000),
|
|
info.codec, info.container, (long long)ctx->src_size);
|
|
vc_progress_set(0);
|
|
ctx->err = vc_run_transcode(ctx);
|
|
if (ctx->err) {
|
|
DEBUG_ERROR(DEBUG_CATEGORY_VIDEO, "video: transcode failed %s -> %s", ctx->src, ctx->dst);
|
|
vc_progress_set(-1);
|
|
} else {
|
|
DEBUG_INFO(DEBUG_CATEGORY_VIDEO, "video: transcode done %s", ctx->dst);
|
|
}
|
|
vc_cleanup(ctx);
|
|
|
|
out:
|
|
if (!ctx->err && video_get_info(ctx->dst, &ctx->result)) ctx->err = -1;
|
|
return;
|
|
}
|
|
|
|
int video_transcode_start(struct media_async* owner, struct UASYNC* ua, const char* src, const char* dst,
|
|
void (*done)(void* arg, int err, const struct video_info* info), void* arg) {
|
|
if (!owner || !ua || !src || !dst || !done) {
|
|
DEBUG_ERROR(DEBUG_CATEGORY_VIDEO, "video: transcode_start invalid args");
|
|
return -1;
|
|
}
|
|
if (vc_active_get()) {
|
|
DEBUG_WARN(DEBUG_CATEGORY_VIDEO, "video: transcode already running");
|
|
return -1;
|
|
}
|
|
|
|
struct vc_ctx* ctx = u_calloc(1, sizeof(*ctx));
|
|
if (!ctx) { DEBUG_ERROR(DEBUG_CATEGORY_VIDEO, "video: transcode context allocation failed"); return -1; }
|
|
snprintf(ctx->src, sizeof(ctx->src), "%s", src);
|
|
snprintf(ctx->dst, sizeof(ctx->dst), "%s", dst);
|
|
ctx->ua = ua;
|
|
ctx->done = done;
|
|
ctx->arg = arg;
|
|
ctx->aud.in_idx = -1;
|
|
|
|
struct stat st;
|
|
ctx->src_size = (stat(src, &st) == 0 && st.st_size > 0) ? (int64_t)st.st_size : 0;
|
|
|
|
vc_active_set(1);
|
|
vc_progress_set(0);
|
|
|
|
media_async_submit(owner, ua, vc_thread_entry, ctx, vc_done_trampoline, ctx);
|
|
return 0;
|
|
}
|
|
|
|
/* ─── превью: первый кадр → JPEG (MJPEG-кодер, один кадр = валидный JPEG) ─── */
|
|
|
|
int video_extract_thumbnail(const char* src, const char* jpg_out, int max_w) {
|
|
if (!src || !src[0] || !jpg_out || !jpg_out[0]) return -1;
|
|
if (max_w <= 0) max_w = 320;
|
|
video_ffmpeg_init();
|
|
|
|
AVFormatContext* fmt = NULL;
|
|
if (avformat_open_input(&fmt, src, NULL, NULL) != 0) {
|
|
DEBUG_WARN(DEBUG_CATEGORY_VIDEO, "video: thumbnail open failed: %s", src);
|
|
return -1;
|
|
}
|
|
if (avformat_find_stream_info(fmt, NULL) < 0) {
|
|
avformat_close_input(&fmt);
|
|
return -1;
|
|
}
|
|
|
|
int vidx = av_find_best_stream(fmt, AVMEDIA_TYPE_VIDEO, -1, -1, NULL, 0);
|
|
if (vidx < 0) { avformat_close_input(&fmt); return -1; }
|
|
AVCodecParameters* par = fmt->streams[vidx]->codecpar;
|
|
const AVCodec* dec = avcodec_find_decoder(par->codec_id);
|
|
if (!dec) { avformat_close_input(&fmt); return -1; }
|
|
AVCodecContext* dctx = avcodec_alloc_context3(dec);
|
|
if (!dctx) { avformat_close_input(&fmt); return -1; }
|
|
if (avcodec_parameters_to_context(dctx, par) < 0 || avcodec_open2(dctx, dec, NULL) < 0) {
|
|
avcodec_free_context(&dctx); avformat_close_input(&fmt); return -1;
|
|
}
|
|
|
|
/* размер превью */
|
|
int tw = dctx->width > 0 ? dctx->width : par->width;
|
|
int th = dctx->height > 0 ? dctx->height : par->height;
|
|
if (tw <= 0 || th <= 0) { avcodec_free_context(&dctx); avformat_close_input(&fmt); return -1; }
|
|
if (tw > max_w) { th = (int)((int64_t)th * max_w / tw); tw = max_w; }
|
|
tw &= ~1; th &= ~1; if (tw < 2) tw = 2; if (th < 2) th = 2;
|
|
|
|
const AVCodec* enc = avcodec_find_encoder(AV_CODEC_ID_MJPEG);
|
|
AVCodecContext* ectx = NULL;
|
|
struct SwsContext* sws = NULL;
|
|
AVFrame* eframe = NULL;
|
|
AVPacket* epkt = NULL;
|
|
int rc = -1;
|
|
if (!enc) goto cleanup;
|
|
ectx = avcodec_alloc_context3(enc);
|
|
if (!ectx) goto cleanup;
|
|
ectx->width = tw; ectx->height = th;
|
|
ectx->pix_fmt = AV_PIX_FMT_YUVJ420P;
|
|
ectx->time_base = (AVRational){1, 25};
|
|
if (avcodec_open2(ectx, enc, NULL) < 0) goto cleanup;
|
|
sws = sws_getContext(dctx->width, dctx->height, dctx->pix_fmt,
|
|
tw, th, AV_PIX_FMT_YUVJ420P, SWS_BILINEAR, NULL, NULL, NULL);
|
|
if (!sws) goto cleanup;
|
|
|
|
AVFrame* frame = av_frame_alloc();
|
|
AVFrame* tmp = av_frame_alloc();
|
|
AVPacket* pkt = av_packet_alloc();
|
|
if (!frame || !tmp || !pkt) { av_frame_free(&frame); av_frame_free(&tmp); av_packet_free(&pkt); goto cleanup; }
|
|
|
|
int done = 0;
|
|
while (!done && av_read_frame(fmt, pkt) >= 0) {
|
|
if (pkt->stream_index != vidx) { av_packet_unref(pkt); continue; }
|
|
if (avcodec_send_packet(dctx, pkt) < 0) { av_packet_unref(pkt); break; }
|
|
if (avcodec_receive_frame(dctx, frame) == 0) {
|
|
tmp->format = AV_PIX_FMT_YUVJ420P;
|
|
tmp->width = tw; tmp->height = th;
|
|
if (av_frame_get_buffer(tmp, 0) < 0) break;
|
|
sws_scale(sws, (const uint8_t* const*)frame->data, frame->linesize,
|
|
0, frame->height, tmp->data, tmp->linesize);
|
|
tmp->pts = 0;
|
|
if (avcodec_send_frame(ectx, tmp) == 0) {
|
|
epkt = av_packet_alloc();
|
|
if (epkt && avcodec_receive_packet(ectx, epkt) == 0) {
|
|
FILE* f = fopen(jpg_out, "wb");
|
|
if (f) {
|
|
fwrite(epkt->data, 1, epkt->size, f);
|
|
fclose(f);
|
|
rc = 0;
|
|
DEBUG_INFO(DEBUG_CATEGORY_VIDEO, "video: thumbnail %s %dx%d", jpg_out, tw, th);
|
|
} else {
|
|
DEBUG_WARN(DEBUG_CATEGORY_VIDEO, "video: thumbnail write failed: %s", jpg_out);
|
|
}
|
|
}
|
|
}
|
|
done = 1;
|
|
}
|
|
av_packet_unref(pkt);
|
|
}
|
|
av_frame_free(&frame); av_frame_free(&tmp); av_packet_free(&pkt);
|
|
|
|
cleanup:
|
|
if (epkt) av_packet_free(&epkt);
|
|
if (sws) sws_freeContext(sws);
|
|
if (ectx) avcodec_free_context(&ectx);
|
|
avcodec_free_context(&dctx);
|
|
avformat_close_input(&fmt);
|
|
return rc;
|
|
}
|
|
|
|
#endif /* HAVE_FFMPEG */
|