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