// 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 #include #include #include #include #ifdef HAVE_FFMPEG #include #include #include #include #include #include #include #include #include #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 */