video: video_extract_thumbnail — превью первого кадра в JPEG (MJPEG)
This commit is contained in:
1 parent
7f3b4f61f4
commit
e264b8aaa7
2 files changed
+110
No files matched your search
@@ -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 */
|
||||
@@ -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
|
||||
|
||||
Reference in new issue
Block a user