diff --git a/src/video/video.c b/src/video/video.c index c136976c..810bf872 100644 --- a/src/video/video.c +++ b/src/video/video.c @@ -52,6 +52,12 @@ int video_transcode_start(struct UASYNC* ua, const char* src, const char* dst, 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 ─── */ @@ -604,4 +610,101 @@ int video_transcode_start(struct UASYNC* ua, const char* src, const char* dst, 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 */ diff --git a/src/video/video.h b/src/video/video.h index 61f5c531..6b13e724 100644 --- a/src/video/video.h +++ b/src/video/video.h @@ -63,6 +63,13 @@ int video_transcode_get_progress(void); /* 1 = транскод выполняется прямо сейчас. */ int video_transcode_is_active(void); +/* + * Извлечь превью: декодирует первый видеокадр, масштабирует до max_w по ширине + * и сохраняет JPEG в jpg_out. Вызывается из любого потока (thread-safe). + * Возвращает 0 при успехе. + */ +int video_extract_thumbnail(const char* src, const char* jpg_out, int max_w); + #ifdef __cplusplus } #endif