You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

710 lines
30 KiB

// video.c — пробинг и транскодирование видео через FFmpeg (опционально, HAVE_FFMPEG)
#include "video.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 <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(); }
#ifndef HAVE_FFMPEG
/* ─── сборка без ffmpeg: стабы ─── */
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 UASYNC* ua, const char* src, const char* dst,
void (*done)(void* arg, int err), void* arg) {
(void)ua; (void)src; (void)dst;
DEBUG_ERROR(DEBUG_CATEGORY_VIDEO, "video: built without FFmpeg (HAVE_FFMPEG undefined)");
if (done) done(arg, -1);
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;
/* Перехват логов FFmpeg: все сообщения идут в наш debug-лог с категорией video,
* иначе ошибки ffmpeg теряются в stderr и не видно причину сбоя транскода. */
static void video_av_log_cb(void* avcl, int level, const char* fmt, va_list vl) {
(void)avcl;
if (level > AV_LOG_WARNING) return; /* отсекаем INFO/VERBOSE/DEBUG/TRACE — демуксер спамит */
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 (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) {
av_log_set_callback(video_av_log_cb);
av_log_set_level(AV_LOG_WARNING);
avformat_network_init();
}
/* строковое описание кода ошибки 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;
pthread_once(&g_ffmpeg_once, ffmpeg_init_once);
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;
pthread_once(&g_ffmpeg_once, ffmpeg_init_once);
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);
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) {
pthread_once(&g_ffmpeg_once, ffmpeg_init_once);
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) {
struct vc_ctx* ctx = (struct vc_ctx*)raw;
vc_active_set(0);
vc_progress_set(-1);
if (ctx->done) ctx->done(ctx->arg, ctx->err);
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:
uasync_post(ctx->ua, vc_done_trampoline, ctx);
return NULL;
}
int video_transcode_start(struct UASYNC* ua, const char* src, const char* dst,
void (*done)(void* arg, int err), void* arg) {
if (!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) 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);
pthread_t tid;
int rc = pthread_create(&tid, NULL, vc_thread_entry, ctx);
if (rc != 0) {
DEBUG_ERROR(DEBUG_CATEGORY_VIDEO, "video: pthread_create failed rc=%d", rc);
vc_active_set(0);
vc_progress_set(-1);
u_free(ctx);
return -1;
}
pthread_detach(tid);
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;
pthread_once(&g_ffmpeg_once, ffmpeg_init_once);
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 */