Browse Source
- src/video/: video_probe/video_get_info/video_transcode_start (FFmpeg, HAVE_FFMPEG) - chat_core/chat_msg: media_video флаг, chat_core_submit_media_message, транскод перед index - headless: команда attach с --video - chatgui: диалог 'Как видео' + прогресс транскода + sendVideoAttachment - FFmpeg в сборке (configure.ac, Makefile.am, chatgui CMake, build_nix.sh) - debug: категория DEBUG_CATEGORY_VIDEOv2
19 changed files with 1026 additions and 29 deletions
@ -0,0 +1,607 @@ |
|||||||
|
// 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; |
||||||
|
} |
||||||
|
|
||||||
|
#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; |
||||||
|
} |
||||||
|
|
||||||
|
#endif /* HAVE_FFMPEG */ |
||||||
@ -0,0 +1,69 @@ |
|||||||
|
// video.h — пробинг и транскодирование видео (FFmpeg, опционально)
|
||||||
|
#ifndef VIDEO_H |
||||||
|
#define VIDEO_H |
||||||
|
|
||||||
|
#ifdef __cplusplus |
||||||
|
extern "C" { |
||||||
|
#endif |
||||||
|
|
||||||
|
#include <stdint.h> |
||||||
|
|
||||||
|
struct UASYNC; |
||||||
|
|
||||||
|
/* ── целевой профиль: H.264 в MP4, не более 720p / 2 Mbit ── */ |
||||||
|
|
||||||
|
#define VIDEO_TARGET_MAX_WIDTH 1280 |
||||||
|
#define VIDEO_TARGET_MAX_HEIGHT 720 |
||||||
|
#define VIDEO_TARGET_MAX_BITRATE 2000000 // 2 Mbit (только видео)
|
||||||
|
#define VIDEO_TARGET_AUDIO_BITRATE 128000 // AAC 128 kbit
|
||||||
|
|
||||||
|
/* ── информация о видеофайле ── */ |
||||||
|
|
||||||
|
struct video_info { |
||||||
|
int is_video; // 1 = открыт и есть видеопоток
|
||||||
|
int width; |
||||||
|
int height; |
||||||
|
int64_t bitrate; // bitrate видеопотока (или контейнера)
|
||||||
|
int64_t duration_ms; |
||||||
|
int has_audio; |
||||||
|
char codec[32]; // напр. "h264", "hevc", "mpeg4"
|
||||||
|
char container[32]; // короткое имя контейнера (ifmt->name)
|
||||||
|
}; |
||||||
|
|
||||||
|
/*
|
||||||
|
* Быстрый пробинг (читает только заголовок). Вызывается из любого потока |
||||||
|
* (в т.ч. из GUI-потока Qt) — thread-safe, не блокирует надолго. |
||||||
|
* Возвращает 1 если файл содержит ffmpeg-совместимый видеопоток, иначе 0. |
||||||
|
*/ |
||||||
|
int video_probe(const char* path); |
||||||
|
|
||||||
|
/* Полное чтение метаданных. Заполняет info. Возвращает 0 при успехе. */ |
||||||
|
int video_get_info(const char* path, struct video_info* out); |
||||||
|
|
||||||
|
/*
|
||||||
|
* Решает, нужен ли транскод: 0 = уже подходит (слать как есть), |
||||||
|
* 1 = нужен транскод. Вызывается из любого потока. |
||||||
|
*/ |
||||||
|
int video_needs_transcode(const struct video_info* info); |
||||||
|
|
||||||
|
/*
|
||||||
|
* Запуск транскодирования. Вызывается из uasync-потока. |
||||||
|
* Создаёт отдельный pthread, который либо перекодирует src->dst в целевой |
||||||
|
* профиль, либо (если транскод не нужен) копирует src->dst как есть. |
||||||
|
* Прогресс обновляется атомарно, читается через video_transcode_get_progress(). |
||||||
|
* По завершении done(arg, err) выполняется в uasync-потоке (через uasync_post). |
||||||
|
*/ |
||||||
|
int video_transcode_start(struct UASYNC* ua, |
||||||
|
const char* src, const char* dst, |
||||||
|
void (*done)(void* arg, int err), void* arg); |
||||||
|
|
||||||
|
/* Прогресс активного транскода в промилле 0..1000, -1 если транскода нет. */ |
||||||
|
int video_transcode_get_progress(void); |
||||||
|
|
||||||
|
/* 1 = транскод выполняется прямо сейчас. */ |
||||||
|
int video_transcode_is_active(void); |
||||||
|
|
||||||
|
#ifdef __cplusplus |
||||||
|
} |
||||||
|
#endif |
||||||
|
#endif /* VIDEO_H */ |
||||||
Loading…
Reference in new issue