Browse Source

video: video_extract_thumbnail — превью первого кадра в JPEG (MJPEG)

v2
evgeny 3 weeks ago
parent
commit
e264b8aaa7
  1. 103
      src/video/video.c
  2. 7
      src/video/video.h

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

7
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

Loading…
Cancel
Save