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
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 */
|
|
|