diff --git a/src/video/video.c b/src/video/video.c index 1411dab7..c815493b 100644 --- a/src/video/video.c +++ b/src/video/video.c @@ -11,6 +11,8 @@ #include #include #include +#include +#include #ifdef HAVE_FFMPEG #include @@ -21,6 +23,7 @@ #include #include #include +#include #include #include #endif @@ -32,8 +35,8 @@ 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); } +static int vc_active_get(void) { return __atomic_load_n(&g_transcode_active, __ATOMIC_ACQUIRE); } +static void vc_active_set(int v) { __atomic_store_n(&g_transcode_active, v, __ATOMIC_RELEASE); } int video_transcode_get_progress(void) { return vc_progress_get(); } int video_transcode_is_active(void) { return vc_active_get(); } @@ -73,7 +76,7 @@ int video_codec_log_level(int level) { void video_ffmpeg_init(void) {} 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_get_info(const char* path, struct video_info* out) { (void)path; if (out) memset(out, 0, sizeof(*out)); return -1; } int video_needs_transcode(const struct video_info* info) { (void)info; return 1; } int video_transcode_start(struct media_async* owner, struct UASYNC* ua, const char* src, const char* dst, @@ -153,620 +156,781 @@ void video_ffmpeg_init(void) { av_log_set_level(AV_LOG_DEBUG); } -/* строковое описание кода ошибки ffmpeg (используется только в worker-потоке) */ -static const char* vc_errstr(int err) { - static char buf[64]; - av_strerror(err, buf, sizeof(buf)); - return buf; +/* Ошибка FFmpeg принадлежит текущему потоку; thumbnail и worker могут работать одновременно. */ +static const char* vc_errstr(int error) { + static __thread char buffer[AV_ERROR_MAX_STRING_SIZE]; + av_strerror(error, buffer, sizeof(buffer)); + return buffer; } -/* ─── пробинг ─── */ +/* Единый выбор потоков для метаданных и подготовки; обложка не является видео. */ +static int vc_stream_index(AVFormatContext* format, enum AVMediaType type) { + int index = av_find_best_stream(format, type, -1, -1, NULL, 0); + if (index >= 0 && type == AVMEDIA_TYPE_VIDEO && (format->streams[index]->disposition & AV_DISPOSITION_ATTACHED_PIC)) + return AVERROR_STREAM_NOT_FOUND; + return index; +} + +static void vc_metadata(AVFormatContext* format, int video, int audio, struct video_info* info) { + memset(info, 0, sizeof(*info)); + AVStream* stream = format->streams[video]; + AVCodecParameters* parameters = stream->codecpar; + info->is_video = 1; info->width = parameters->width; info->height = parameters->height; + info->bitrate = parameters->bit_rate > 0 ? parameters->bit_rate : 0; + info->is_yuv420p = parameters->format == AV_PIX_FMT_YUV420P; + if (format->duration != AV_NOPTS_VALUE && format->duration > 0) info->duration_ms = format->duration / 1000; + else if (stream->duration != AV_NOPTS_VALUE && stream->duration > 0) + info->duration_ms = av_rescale_q(stream->duration, stream->time_base, (AVRational){1, 1000}); + const AVCodecDescriptor* descriptor = avcodec_descriptor_get(parameters->codec_id); + if (descriptor) snprintf(info->codec, sizeof(info->codec), "%s", descriptor->name); + if (format->iformat) snprintf(info->container, sizeof(info->container), "%s", format->iformat->name); + info->has_audio = audio >= 0; + if (audio >= 0) { + parameters = format->streams[audio]->codecpar; + info->audio_bitrate = parameters->bit_rate > 0 ? parameters->bit_rate : 0; + descriptor = avcodec_descriptor_get(parameters->codec_id); + if (descriptor) snprintf(info->audio_codec, sizeof(info->audio_codec), "%s", descriptor->name); + } +} int video_probe(const char* path) { - if (!path || !path[0]) return 0; - video_ffmpeg_init(); - - 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; + struct video_info info; + return video_get_info(path, &info) == 0; } int video_get_info(const char* path, struct video_info* out) { - if (!out) return -1; + if (!out) { DEBUG_ERROR(DEBUG_CATEGORY_VIDEO, "video metadata requires output"); return -1; } memset(out, 0, sizeof(*out)); - if (!path || !path[0]) return -1; + if (!path || !*path) { DEBUG_ERROR(DEBUG_CATEGORY_VIDEO, "video metadata requires path"); return -1; } video_ffmpeg_init(); - - AVFormatContext* fmt = NULL; - if (avformat_open_input(&fmt, path, NULL, NULL) != 0) { - DEBUG_ERROR(DEBUG_CATEGORY_VIDEO, "video: open failed: %s", path); + AVFormatContext* format = NULL; + int error = avformat_open_input(&format, path, NULL, NULL); + if (error >= 0) error = avformat_find_stream_info(format, NULL); + int video = error >= 0 ? vc_stream_index(format, AVMEDIA_TYPE_VIDEO) : error; + if (video < 0) { + DEBUG_WARN(DEBUG_CATEGORY_VIDEO, "video metadata failed path=%s error=%s", path, vc_errstr(video)); + avformat_close_input(&format); 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); + vc_metadata(format, video, vc_stream_index(format, AVMEDIA_TYPE_AUDIO), out); + avformat_close_input(&format); + DEBUG_DEBUG(DEBUG_CATEGORY_VIDEO, "video metadata path=%s dimensions=%dx%d codec=%s bitrate=%lld audio=%s/%lld", + path, out->width, out->height, out->codec, (long long)out->bitrate, + out->audio_codec, (long long)out->audio_bitrate); 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; + if (!info || !info->is_video || !info->is_yuv420p || strcmp(info->codec, "h264") || + info->width < 2 || info->height < 2 || info->width > VIDEO_TARGET_MAX_WIDTH || + info->height > VIDEO_TARGET_MAX_HEIGHT || info->bitrate <= 0 || info->bitrate > VIDEO_TARGET_MAX_BITRATE) return 1; + /* Эти demuxers предоставляют AVC/AAC codec configuration для прямой записи MP4 без дополнительных BSF. */ + if (!strstr(info->container, "mov") && !strstr(info->container, "matroska")) return 1; + if (info->has_audio && (strcmp(info->audio_codec, "aac") || + info->audio_bitrate <= 0 || info->audio_bitrate > VIDEO_TARGET_AUDIO_BITRATE)) return 1; return 0; } -/* ─── транскодирование ─── */ +struct vc_file { + char temporary[VIDEO_PATH_MAX + 64]; + FILE* file; +}; + +#ifdef _WIN32 +static int vc_wide(const char* path, wchar_t* wide) { + if (MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, path, -1, wide, VIDEO_PATH_MAX + 64)) return 0; + DEBUG_ERROR(DEBUG_CATEGORY_VIDEO, "video invalid UTF-8 path=%s winerr=%lu", path, GetLastError()); + return -1; +} +#endif + +/* Проверить обычные файлы и их идентичность до записи; повторить перед публикацией. */ +static int vc_paths(const char* source, const char* destination) { + if (!source || !destination || !*source || !*destination || + strlen(source) >= VIDEO_PATH_MAX || strlen(destination) >= VIDEO_PATH_MAX) { + DEBUG_ERROR(DEBUG_CATEGORY_VIDEO, "video invalid/oversized source or destination"); + return -1; + } +#ifdef _WIN32 + wchar_t from[VIDEO_PATH_MAX + 64], to[VIDEO_PATH_MAX + 64]; + if (vc_wide(source, from) || vc_wide(destination, to)) return -1; + HANDLE input = CreateFileW(from, GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, + NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); + BY_HANDLE_FILE_INFORMATION a, b; + int valid = input != INVALID_HANDLE_VALUE && GetFileInformationByHandle(input, &a) && + GetFileType(input) == FILE_TYPE_DISK && !(a.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY); + if (input != INVALID_HANDLE_VALUE) CloseHandle(input); + if (!valid) { DEBUG_ERROR(DEBUG_CATEGORY_VIDEO, "video invalid source=%s winerr=%lu", source, GetLastError()); return -1; } + DWORD attributes = GetFileAttributesW(to); + if (attributes == INVALID_FILE_ATTRIBUTES) { + DWORD error = GetLastError(); + if (error == ERROR_FILE_NOT_FOUND) return 0; + DEBUG_ERROR(DEBUG_CATEGORY_VIDEO, "video destination inspection failed path=%s winerr=%lu", destination, error); + return -1; + } + if (attributes & (FILE_ATTRIBUTE_DIRECTORY | FILE_ATTRIBUTE_REPARSE_POINT)) { + DEBUG_ERROR(DEBUG_CATEGORY_VIDEO, "video destination is directory/reparse point path=%s", destination); return -1; + } + HANDLE output = CreateFileW(to, GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, + NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); + valid = output != INVALID_HANDLE_VALUE && GetFileInformationByHandle(output, &b) && GetFileType(output) == FILE_TYPE_DISK; + if (output != INVALID_HANDLE_VALUE) CloseHandle(output); + if (!valid || (a.dwVolumeSerialNumber == b.dwVolumeSerialNumber && + a.nFileIndexHigh == b.nFileIndexHigh && a.nFileIndexLow == b.nFileIndexLow)) { + DEBUG_ERROR(DEBUG_CATEGORY_VIDEO, "video destination aliases source or cannot be inspected path=%s", destination); return -1; + } +#else + struct stat input, output; + if (stat(source, &input) || !S_ISREG(input.st_mode)) { + DEBUG_ERROR(DEBUG_CATEGORY_VIDEO, "video source is not readable regular file path=%s errno=%d", source, errno); return -1; + } + if (lstat(destination, &output)) { + if (errno == ENOENT) return 0; + DEBUG_ERROR(DEBUG_CATEGORY_VIDEO, "video destination stat failed path=%s errno=%d", destination, errno); return -1; + } + if (!S_ISREG(output.st_mode) || (input.st_dev == output.st_dev && input.st_ino == output.st_ino)) { + DEBUG_ERROR(DEBUG_CATEGORY_VIDEO, "video destination is not distinct regular file source=%s destination=%s", source, destination); + return -1; + } +#endif + return 0; +} + +/* Создать уникальный соседний файл эксклюзивно; FILE остаётся у задачи до finish/discard. */ +static int vc_file_open(struct vc_file* output, const char* destination) { + for (int attempt = 0; attempt < 8; attempt++) { + uint8_t random[16]; char suffix[33]; + if (random_bytes(random, sizeof(random))) { DEBUG_ERROR(DEBUG_CATEGORY_VIDEO, "video temp randomness failed"); return -1; } + for (unsigned i = 0; i < sizeof(random); i++) snprintf(suffix + i * 2, 3, "%02x", random[i]); + snprintf(output->temporary, sizeof(output->temporary), "%s.tmp.%s", destination, suffix); +#ifdef _WIN32 + wchar_t wide[VIDEO_PATH_MAX + 64]; + if (vc_wide(output->temporary, wide)) { output->temporary[0] = 0; return -1; } + int fd = _wopen(wide, _O_CREAT | _O_EXCL | _O_RDWR | _O_BINARY, _S_IREAD | _S_IWRITE); +#else + int fd = open(output->temporary, O_CREAT | O_EXCL | O_RDWR, 0600); +#endif + if (fd < 0) { + output->temporary[0] = 0; + if (errno == EEXIST) continue; + DEBUG_ERROR(DEBUG_CATEGORY_VIDEO, "video temporary creation failed destination=%s errno=%d", destination, errno); + return -1; + } +#ifdef _WIN32 + output->file = _fdopen(fd, "wb+"); + if (!output->file) _close(fd); +#else + output->file = fdopen(fd, "wb+"); + if (!output->file) close(fd); +#endif + if (!output->file) { DEBUG_ERROR(DEBUG_CATEGORY_VIDEO, "video fdopen failed errno=%d", errno); return -1; } + DEBUG_DEBUG(DEBUG_CATEGORY_VIDEO, "video temporary opened path=%s", output->temporary); + return 0; + } + DEBUG_ERROR(DEBUG_CATEGORY_VIDEO, "video temporary collision limit destination=%s", destination); + return -1; +} + +static int vc_file_finish(struct vc_file* output) { + int error = fflush(output->file) ? errno : 0; + if (fclose(output->file) && !error) error = errno; + output->file = NULL; + if (error) { DEBUG_ERROR(DEBUG_CATEGORY_VIDEO, "video flush/close failed path=%s errno=%d", output->temporary, error); return -1; } + return 0; +} + +static void vc_file_discard(struct vc_file* output) { + if (output->file) { + if (fclose(output->file)) DEBUG_WARN(DEBUG_CATEGORY_VIDEO, "video cleanup close failed errno=%d", errno); + output->file = NULL; + } + if (!output->temporary[0]) return; +#ifdef _WIN32 + wchar_t wide[VIDEO_PATH_MAX + 64]; + int error = vc_wide(output->temporary, wide) ? -1 : _wremove(wide); +#else + int error = remove(output->temporary); +#endif + if (error && errno != ENOENT) DEBUG_WARN(DEBUG_CATEGORY_VIDEO, "video temporary cleanup failed path=%s errno=%d", output->temporary, errno); + output->temporary[0] = 0; +} + +/* Последняя способная отказать операция: после rename результат уже опубликован. */ +static int vc_file_publish(struct vc_file* output, const char* source, const char* destination) { + if (vc_paths(source, destination)) return -1; +#ifdef _WIN32 + wchar_t from[VIDEO_PATH_MAX + 64], to[VIDEO_PATH_MAX + 64]; + if (vc_wide(output->temporary, from) || vc_wide(destination, to)) return -1; + if (!MoveFileExW(from, to, MOVEFILE_REPLACE_EXISTING)) { + DEBUG_ERROR(DEBUG_CATEGORY_VIDEO, "video publish failed destination=%s winerr=%lu", destination, GetLastError()); return -1; + } +#else + if (rename(output->temporary, destination)) { + DEBUG_ERROR(DEBUG_CATEGORY_VIDEO, "video publish failed destination=%s errno=%d", destination, errno); return -1; + } +#endif + output->temporary[0] = 0; + DEBUG_INFO(DEBUG_CATEGORY_VIDEO, "video published destination=%s", destination); + 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 энкодера) */ + int index; + AVRational input_time_base; + AVCodecContext* decoder; + AVCodecContext* encoder; + AVStream* output; + struct SwsContext* scale; + struct SwrContext* resample; + AVAudioFifo* fifo; + AVFrame* frame; + int64_t next_pts, first_pts, input_samples, last_pts, frame_duration; + unsigned frames, packets; }; struct vc_ctx { - char src[1024]; - char dst[1024]; - struct UASYNC* ua; - void (*done)(void* arg, int err, const struct video_info* info); + char source[VIDEO_PATH_MAX], destination[VIDEO_PATH_MAX]; + struct vc_file file; + void (*done)(void*, int, const struct video_info*); + void* arg; + int error, remux; + int64_t origin_us, source_size; + AVFormatContext* input; + AVFormatContext* output; + struct vc_stream video, audio; struct video_info result; - 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; +/* Custom AVIO пишет в уже эксклюзивно открытый файл, без повторного открытия временного пути. */ +static int vc_io_write(void* opaque, uint8_t* bytes, int length) { + struct vc_file* file = opaque; + if (fwrite(bytes, 1, length, file->file) == (size_t)length) return length; + int error = errno ? errno : EIO; + DEBUG_ERROR(DEBUG_CATEGORY_VIDEO, "video MP4 write failed path=%s requested=%d errno=%d", file->temporary, length, error); + return AVERROR(error); } -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; +static int64_t vc_io_seek(void* opaque, int64_t offset, int whence) { + struct vc_file* file = opaque; + if (whence == AVSEEK_SIZE) { + struct stat info; + if (!fstat(fileno(file->file), &info)) return info.st_size; + } else { +#ifdef _WIN32 + if (!_fseeki64(file->file, offset, whence & ~AVSEEK_FORCE)) return _ftelli64(file->file); +#else + if (!fseeko(file->file, offset, whence & ~AVSEEK_FORCE)) return ftello(file->file); +#endif } - 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; + int error = errno ? errno : EIO; + DEBUG_ERROR(DEBUG_CATEGORY_VIDEO, "video MP4 seek failed offset=%lld whence=%d errno=%d", (long long)offset, whence, error); + return AVERROR(error); } -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 void vc_stream_close(struct vc_stream* stream) { + sws_freeContext(stream->scale); + swr_free(&stream->resample); + av_audio_fifo_free(stream->fifo); + av_frame_free(&stream->frame); + avcodec_free_context(&stream->decoder); + avcodec_free_context(&stream->encoder); } -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; +/* Финализировать AVIO до fclose; буфер custom IO принадлежит вызывающему. */ +static int vc_output_close(struct vc_ctx* ctx) { + int error = 0; + if (ctx->output && ctx->output->pb) { + avio_flush(ctx->output->pb); + error = ctx->output->pb->error; + av_freep(&ctx->output->pb->buffer); + avio_context_free(&ctx->output->pb); + if (error < 0) DEBUG_ERROR(DEBUG_CATEGORY_VIDEO, "video MP4 buffered output failed error=%s", vc_errstr(error)); } - 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) { - video_ffmpeg_init(); - - 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; + if (ctx->file.file && vc_file_finish(&ctx->file)) error = -1; + return error < 0 ? -1 : 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; } + vc_output_close(ctx); + vc_stream_close(&ctx->video); vc_stream_close(&ctx->audio); + avformat_free_context(ctx->output); ctx->output = NULL; + avformat_close_input(&ctx->input); } -static void vc_done_trampoline(void* raw, int error) { - struct vc_ctx* ctx = (struct vc_ctx*)raw; - vc_active_set(0); - vc_progress_set(-1); - if (ctx->done) ctx->done(ctx->arg, error ? error : ctx->err, &ctx->result); - u_free(ctx); +/* Опубликовать encoder context в mux только после полной успешной настройки. */ +static int vc_output_stream(struct vc_ctx* ctx, struct vc_stream* stream) { + stream->output = avformat_new_stream(ctx->output, NULL); + if (!stream->output) return AVERROR(ENOMEM); + int error = avcodec_parameters_from_context(stream->output->codecpar, stream->encoder); + stream->output->time_base = stream->encoder->time_base; + return error; } -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: - if (!ctx->err && video_get_info(ctx->dst, &ctx->result)) ctx->err = -1; - return; -} - -int video_transcode_start(struct media_async* owner, struct UASYNC* ua, const char* src, const char* dst, - void (*done)(void* arg, int err, const struct video_info* info), void* arg) { - if (!owner || !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) { DEBUG_ERROR(DEBUG_CATEGORY_VIDEO, "video: transcode context allocation failed"); 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); - - media_async_submit(owner, ua, vc_thread_entry, ctx, vc_done_trampoline, ctx); +static int vc_open_decoder(struct vc_ctx* ctx, struct vc_stream* stream) { + AVStream* input = ctx->input->streams[stream->index]; + const AVCodec* codec = avcodec_find_decoder(input->codecpar->codec_id); + if (!codec) return AVERROR_DECODER_NOT_FOUND; + stream->decoder = avcodec_alloc_context3(codec); + if (!stream->decoder) return AVERROR(ENOMEM); + int error = avcodec_parameters_to_context(stream->decoder, input->codecpar); + if (error < 0) return error; + stream->decoder->pkt_timebase = input->time_base; + error = avcodec_open2(stream->decoder, codec, NULL); + if (error < 0) return error; + stream->frame = av_frame_alloc(); + if (!stream->frame) return AVERROR(ENOMEM); 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; - video_ffmpeg_init(); - - 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; +static int vc_open_video(struct vc_ctx* ctx) { + struct vc_stream* stream = &ctx->video; + AVCodecContext* decoder = stream->decoder; + if (decoder->width < 2 || decoder->height < 2 || + stream->input_time_base.num <= 0 || stream->input_time_base.den <= 0) return AVERROR_INVALIDDATA; + const AVCodec* codec = avcodec_find_encoder_by_name("libx264"); + if (!codec) codec = avcodec_find_encoder(AV_CODEC_ID_H264); + if (!codec) return AVERROR_ENCODER_NOT_FOUND; + stream->encoder = avcodec_alloc_context3(codec); + if (!stream->encoder) return AVERROR(ENOMEM); + AVCodecContext* encoder = stream->encoder; + double factor = FFMIN(1.0, FFMIN((double)VIDEO_TARGET_MAX_WIDTH / decoder->width, + (double)VIDEO_TARGET_MAX_HEIGHT / decoder->height)); + encoder->width = FFMAX(2, (int)(decoder->width * factor) & ~1); + encoder->height = FFMAX(2, (int)(decoder->height * factor) & ~1); + encoder->pix_fmt = AV_PIX_FMT_YUV420P; encoder->time_base = stream->input_time_base; + encoder->framerate = av_guess_frame_rate(ctx->input, ctx->input->streams[stream->index], NULL); + if (encoder->framerate.num <= 0 || encoder->framerate.den <= 0) { + DEBUG_WARN(DEBUG_CATEGORY_VIDEO, "video unknown frame rate; using 25fps GOP estimate"); + encoder->framerate = (AVRational){25, 1}; } - if (avformat_find_stream_info(fmt, NULL) < 0) { - avformat_close_input(&fmt); - return -1; + encoder->gop_size = 48; encoder->max_b_frames = 0; + encoder->bit_rate = encoder->rc_max_rate = VIDEO_TARGET_MAX_BITRATE; + encoder->rc_buffer_size = 2 * VIDEO_TARGET_MAX_BITRATE; encoder->thread_count = 2; + encoder->sample_aspect_ratio = decoder->sample_aspect_ratio; + if (ctx->output->oformat->flags & AVFMT_GLOBALHEADER) encoder->flags |= AV_CODEC_FLAG_GLOBAL_HEADER; + AVDictionary* options = NULL; + if (!strcmp(codec->name, "libx264")) { + av_dict_set(&options, "preset", "medium", 0); av_dict_set(&options, "crf", "23", 0); } + int error = avcodec_open2(encoder, codec, &options); av_dict_free(&options); + if (error < 0) return error; + stream->frame_duration = FFMAX(1, av_rescale_q(1, av_inv_q(encoder->framerate), encoder->time_base)); + return vc_output_stream(ctx, stream); +} - 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; +static int vc_open_audio(struct vc_ctx* ctx) { + struct vc_stream* stream = &ctx->audio; + AVCodecContext* decoder = stream->decoder; + if (decoder->sample_rate <= 0 || decoder->ch_layout.nb_channels <= 0) return AVERROR_INVALIDDATA; + const AVCodec* codec = avcodec_find_encoder(AV_CODEC_ID_AAC); + if (!codec) return AVERROR_ENCODER_NOT_FOUND; + stream->encoder = avcodec_alloc_context3(codec); + if (!stream->encoder) return AVERROR(ENOMEM); + AVCodecContext* encoder = stream->encoder; + encoder->bit_rate = VIDEO_TARGET_AUDIO_BITRATE; encoder->sample_rate = decoder->sample_rate; + if (codec->supported_samplerates) { + encoder->sample_rate = codec->supported_samplerates[0]; + for (const int* rate = codec->supported_samplerates; *rate; rate++) + if (abs(*rate - decoder->sample_rate) < abs(encoder->sample_rate - decoder->sample_rate)) encoder->sample_rate = *rate; } + encoder->sample_fmt = codec->sample_fmts ? codec->sample_fmts[0] : AV_SAMPLE_FMT_FLTP; + int error = av_channel_layout_copy(&encoder->ch_layout, &decoder->ch_layout); + if (error < 0) return error; + encoder->time_base = (AVRational){1, encoder->sample_rate}; + if (ctx->output->oformat->flags & AVFMT_GLOBALHEADER) encoder->flags |= AV_CODEC_FLAG_GLOBAL_HEADER; + error = avcodec_open2(encoder, codec, NULL); + if (error < 0) return error; + if (encoder->frame_size <= 0) return AVERROR_INVALIDDATA; + error = swr_alloc_set_opts2(&stream->resample, &encoder->ch_layout, encoder->sample_fmt, encoder->sample_rate, + &decoder->ch_layout, decoder->sample_fmt, decoder->sample_rate, 0, NULL); + if (error < 0) return error; + error = swr_init(stream->resample); + if (error < 0) return error; + stream->fifo = av_audio_fifo_alloc(encoder->sample_fmt, encoder->ch_layout.nb_channels, encoder->frame_size); + if (!stream->fifo) return AVERROR(ENOMEM); + return vc_output_stream(ctx, stream); +} - /* размер превью */ - 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; +/* Получить все доступные пакеты. EAGAIN после принятого drain — ошибка состояния, а не повод вращать цикл. */ +static int vc_encode_receive(struct vc_ctx* ctx, struct vc_stream* stream, int drain) { + AVPacket* packet = av_packet_alloc(); + if (!packet) return AVERROR(ENOMEM); + int error; + while ((error = avcodec_receive_packet(stream->encoder, packet)) == 0) { + if (stream == &ctx->video && packet->duration <= 0) packet->duration = stream->frame_duration; + av_packet_rescale_ts(packet, stream->encoder->time_base, stream->output->time_base); + packet->stream_index = stream->output->index; packet->pos = -1; + error = av_interleaved_write_frame(ctx->output, packet); + av_packet_unref(packet); + if (error < 0) break; + stream->packets++; + } + av_packet_free(&packet); + if (error == AVERROR_EOF || (error == AVERROR(EAGAIN) && !drain)) return 0; + return error; +} - 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; +static int vc_encode_send(struct vc_ctx* ctx, struct vc_stream* stream, AVFrame* frame) { + int error = avcodec_send_frame(stream->encoder, frame); + if (error == AVERROR(EAGAIN)) { + error = vc_encode_receive(ctx, stream, 0); + if (error < 0) return error; + error = avcodec_send_frame(stream->encoder, frame); + } + if (error < 0) return error; + return vc_encode_receive(ctx, stream, frame == NULL); +} +/* Отправлять AAC ровно frame_size сэмплов, кроме разрешённого последнего неполного блока. */ +static int vc_audio_encode(struct vc_ctx* ctx, int drain) { + struct vc_stream* stream = &ctx->audio; + int count; + while ((count = av_audio_fifo_size(stream->fifo)) >= stream->encoder->frame_size || (drain && count)) { + AVFrame* frame = av_frame_alloc(); + if (!frame) return AVERROR(ENOMEM); + frame->format = stream->encoder->sample_fmt; frame->sample_rate = stream->encoder->sample_rate; + frame->nb_samples = FFMIN(count, stream->encoder->frame_size); frame->pts = stream->next_pts; + int error = av_channel_layout_copy(&frame->ch_layout, &stream->encoder->ch_layout); + if (error >= 0) error = av_frame_get_buffer(frame, 0); + if (error >= 0 && av_audio_fifo_read(stream->fifo, (void**)frame->extended_data, frame->nb_samples) != frame->nb_samples) + error = AVERROR(EIO); + if (error >= 0) error = vc_encode_send(ctx, stream, frame); + stream->next_pts += frame->nb_samples; + av_frame_free(&frame); + if (error < 0) return error; + } + return 0; +} + +/* Ресемплер и FIFO обслуживаются также на EOF, чтобы не потерять хвост аудио. */ +static int vc_audio_convert(struct vc_ctx* ctx, AVFrame* input) { + struct vc_stream* stream = &ctx->audio; + AVCodecContext* encoder = stream->encoder; + int64_t delay = swr_get_delay(stream->resample, stream->decoder->sample_rate); + int64_t needed = av_rescale_rnd(delay + (input ? input->nb_samples : 0), + encoder->sample_rate, stream->decoder->sample_rate, AV_ROUND_UP); + if (!input && !needed) return 0; + if (needed <= 0 || needed > INT_MAX) return AVERROR_INVALIDDATA; 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; } + if (!frame) return AVERROR(ENOMEM); + frame->format = encoder->sample_fmt; frame->sample_rate = encoder->sample_rate; frame->nb_samples = (int)needed; + int error = av_channel_layout_copy(&frame->ch_layout, &encoder->ch_layout); + if (error >= 0) error = av_frame_get_buffer(frame, 0); + if (error >= 0) error = swr_convert(stream->resample, frame->extended_data, frame->nb_samples, + input ? (const uint8_t**)input->extended_data : NULL, input ? input->nb_samples : 0); + int produced = error; + if (error > 0 && av_audio_fifo_write(stream->fifo, (void**)frame->extended_data, produced) != produced) error = AVERROR(ENOMEM); + av_frame_free(&frame); + if (error < 0) return error; + error = vc_audio_encode(ctx, 0); + return error < 0 ? error : produced; +} - 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); - } - } +static int vc_frame(struct vc_ctx* ctx, struct vc_stream* stream) { + AVFrame* input = stream->frame; + int64_t timestamp = input->best_effort_timestamp; + if (stream == &ctx->audio) { + if (!stream->frames) { + stream->first_pts = timestamp == AV_NOPTS_VALUE ? 0 : + av_rescale_q(timestamp, stream->input_time_base, stream->encoder->time_base) - + av_rescale_q(ctx->origin_us, AV_TIME_BASE_Q, stream->encoder->time_base); + stream->next_pts = stream->first_pts; + } else if (timestamp != AV_NOPTS_VALUE) { + int64_t actual = av_rescale_q(timestamp, stream->input_time_base, stream->encoder->time_base) - + av_rescale_q(ctx->origin_us, AV_TIME_BASE_Q, stream->encoder->time_base); + int64_t expected = stream->first_pts + av_rescale_q(stream->input_samples, + (AVRational){1, stream->decoder->sample_rate}, stream->encoder->time_base); + if (llabs(actual - expected) > stream->encoder->sample_rate / 500 + 2) { + DEBUG_ERROR(DEBUG_CATEGORY_VIDEO, "video audio timestamp discontinuity expected=%lld actual=%lld", + (long long)expected, (long long)actual); + return AVERROR_INVALIDDATA; } - done = 1; } - av_packet_unref(pkt); + stream->frames++; stream->input_samples += input->nb_samples; + return vc_audio_convert(ctx, input); } - av_frame_free(&frame); av_frame_free(&tmp); av_packet_free(&pkt); + if (input->width <= 0 || input->height <= 0) return AVERROR_INVALIDDATA; + AVFrame* frame = av_frame_alloc(); + if (!frame) return AVERROR(ENOMEM); + frame->format = stream->encoder->pix_fmt; frame->width = stream->encoder->width; frame->height = stream->encoder->height; + frame->sample_aspect_ratio = stream->encoder->sample_aspect_ratio; + int error = av_frame_get_buffer(frame, 0); + if (error >= 0) { + stream->scale = sws_getCachedContext(stream->scale, input->width, input->height, input->format, + frame->width, frame->height, frame->format, SWS_BILINEAR, NULL, NULL, NULL); + if (!stream->scale) error = AVERROR(ENOMEM); + else if (sws_scale(stream->scale, (const uint8_t* const*)input->data, input->linesize, 0, + input->height, frame->data, frame->linesize) != frame->height) error = AVERROR_INVALIDDATA; + } + frame->pts = timestamp == AV_NOPTS_VALUE ? + (stream->last_pts == AV_NOPTS_VALUE ? 0 : stream->last_pts + stream->frame_duration) : + av_rescale_q(timestamp, stream->input_time_base, stream->encoder->time_base) - + av_rescale_q(ctx->origin_us, AV_TIME_BASE_Q, stream->encoder->time_base); + if (stream->last_pts != AV_NOPTS_VALUE && frame->pts <= stream->last_pts) error = AVERROR_INVALIDDATA; + stream->last_pts = frame->pts; stream->frames++; + if (error >= 0) error = vc_encode_send(ctx, stream, frame); + av_frame_free(&frame); + return error; +} -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; +static int vc_decode_receive(struct vc_ctx* ctx, struct vc_stream* stream, int drain) { + int error; + while ((error = avcodec_receive_frame(stream->decoder, stream->frame)) == 0) { + error = vc_frame(ctx, stream); + av_frame_unref(stream->frame); + if (error < 0) return error; + } + if (error == AVERROR_EOF || (error == AVERROR(EAGAIN) && !drain)) return 0; + return error; +} + +static int vc_decode_send(struct vc_ctx* ctx, struct vc_stream* stream, AVPacket* packet) { + int error = avcodec_send_packet(stream->decoder, packet); + if (error == AVERROR(EAGAIN)) { + error = vc_decode_receive(ctx, stream, 0); + if (error < 0) return error; + error = avcodec_send_packet(stream->decoder, packet); + } + if (error < 0) return error; + return vc_decode_receive(ctx, stream, packet == NULL); +} + +/* Либо переносить выбранные AVC/AAC-потоки, либо полностью настроить обе цепочки перекодирования. */ +static int vc_prepare_streams(struct vc_ctx* ctx, const struct video_info* info) { + ctx->remux = !video_needs_transcode(info); + struct vc_stream* streams[] = {&ctx->video, &ctx->audio}; + for (unsigned i = 0; i < 2; i++) { + struct vc_stream* stream = streams[i]; + if (stream->index < 0) continue; + stream->input_time_base = ctx->input->streams[stream->index]->time_base; + stream->last_pts = AV_NOPTS_VALUE; + if (stream->input_time_base.num <= 0 || stream->input_time_base.den <= 0) return AVERROR_INVALIDDATA; + if (ctx->remux) { + stream->output = avformat_new_stream(ctx->output, NULL); + if (!stream->output) return AVERROR(ENOMEM); + int error = avcodec_parameters_copy(stream->output->codecpar, ctx->input->streams[stream->index]->codecpar); + if (error < 0) return error; + stream->output->codecpar->codec_tag = 0; stream->output->time_base = stream->input_time_base; + } else { + int error = vc_open_decoder(ctx, stream); + if (error >= 0) error = i ? vc_open_audio(ctx) : vc_open_video(ctx); + if (error < 0) { + DEBUG_ERROR(DEBUG_CATEGORY_VIDEO, "video stream setup failed type=%s error=%s", i ? "audio" : "video", vc_errstr(error)); + return error; + } + } + } + return 0; +} + +static int vc_run(struct vc_ctx* ctx) { + int error = avformat_open_input(&ctx->input, ctx->source, NULL, NULL); + if (error >= 0) error = avformat_find_stream_info(ctx->input, NULL); + if (error < 0) return error; + ctx->video.index = vc_stream_index(ctx->input, AVMEDIA_TYPE_VIDEO); + ctx->audio.index = vc_stream_index(ctx->input, AVMEDIA_TYPE_AUDIO); + if (ctx->video.index < 0) return ctx->video.index; + ctx->origin_us = ctx->input->start_time == AV_NOPTS_VALUE ? 0 : ctx->input->start_time; + struct video_info info; vc_metadata(ctx->input, ctx->video.index, ctx->audio.index, &info); + error = avformat_alloc_output_context2(&ctx->output, NULL, "mp4", ctx->file.temporary); + if (error >= 0) error = vc_prepare_streams(ctx, &info); + if (error < 0) return error; + uint8_t* buffer = av_malloc(32768); + if (!buffer) return AVERROR(ENOMEM); + ctx->output->pb = avio_alloc_context(buffer, 32768, 1, &ctx->file, NULL, vc_io_write, vc_io_seek); + if (!ctx->output->pb) { av_free(buffer); return AVERROR(ENOMEM); } + ctx->output->flags |= AVFMT_FLAG_CUSTOM_IO; + error = avformat_write_header(ctx->output, NULL); + if (error < 0) return error; + DEBUG_INFO(DEBUG_CATEGORY_VIDEO, "video preparation mode=%s source=%s audio=%d dimensions=%dx%d", + ctx->remux ? "remux" : "transcode", ctx->source, info.has_audio, info.width, info.height); + AVPacket* packet = av_packet_alloc(); + if (!packet) return AVERROR(ENOMEM); + int64_t bytes = 0; + while ((error = av_read_frame(ctx->input, packet)) >= 0) { + struct vc_stream* stream = packet->stream_index == ctx->video.index ? &ctx->video : + packet->stream_index == ctx->audio.index ? &ctx->audio : NULL; + bytes += packet->size; + if (ctx->source_size > 0) vc_progress_set((int)FFMIN(990, av_rescale(bytes, 990, ctx->source_size))); + if (packet->flags & AV_PKT_FLAG_CORRUPT) error = AVERROR_INVALIDDATA; + else if (stream && ctx->remux) { + int64_t origin = av_rescale_q(ctx->origin_us, AV_TIME_BASE_Q, stream->input_time_base); + if (packet->pts != AV_NOPTS_VALUE) packet->pts -= origin; + if (packet->dts != AV_NOPTS_VALUE) packet->dts -= origin; + av_packet_rescale_ts(packet, stream->input_time_base, stream->output->time_base); + packet->stream_index = stream->output->index; packet->pos = -1; + error = av_interleaved_write_frame(ctx->output, packet); + if (error >= 0) stream->packets++; + } else if (stream) error = vc_decode_send(ctx, stream, packet); + av_packet_unref(packet); + if (error < 0) break; + } + av_packet_free(&packet); + if (error != AVERROR_EOF) return error < 0 ? error : AVERROR_INVALIDDATA; + if (!ctx->remux) { + if ((error = vc_decode_send(ctx, &ctx->video, NULL)) < 0) return error; + if (ctx->audio.index >= 0) { + if ((error = vc_decode_send(ctx, &ctx->audio, NULL)) < 0) return error; + do { error = vc_audio_convert(ctx, NULL); } while (error > 0); + if (error < 0 || (error = vc_audio_encode(ctx, 1)) < 0) return error; + } + if ((error = vc_encode_send(ctx, &ctx->video, NULL)) < 0) return error; + if (ctx->audio.index >= 0 && (error = vc_encode_send(ctx, &ctx->audio, NULL)) < 0) return error; + } + if (!ctx->video.packets || (ctx->audio.index >= 0 && !ctx->audio.packets)) return AVERROR_INVALIDDATA; + if ((error = av_write_trailer(ctx->output)) < 0) return error; + if (vc_output_close(ctx)) return AVERROR(EIO); + DEBUG_INFO(DEBUG_CATEGORY_VIDEO, "video prepared frames=%u video_packets=%u audio_frames=%u samples=%lld audio_packets=%u", + ctx->video.frames, ctx->video.packets, ctx->audio.frames, (long long)ctx->audio.input_samples, ctx->audio.packets); + return 0; +} + +static void vc_thread_entry(void* raw) { + struct vc_ctx* ctx = raw; + video_ffmpeg_init(); + if (vc_paths(ctx->source, ctx->destination) || vc_file_open(&ctx->file, ctx->destination)) { ctx->error = -1; return; } + struct stat info; + if (!stat(ctx->source, &info)) ctx->source_size = info.st_size; + int error = vc_run(ctx); + vc_cleanup(ctx); + if (error >= 0) error = video_get_info(ctx->file.temporary, &ctx->result); + if (error >= 0 && (strcmp(ctx->result.codec, "h264") || !ctx->result.is_yuv420p || + ctx->result.width > VIDEO_TARGET_MAX_WIDTH || ctx->result.height > VIDEO_TARGET_MAX_HEIGHT || + (ctx->audio.index >= 0 && (!ctx->result.has_audio || strcmp(ctx->result.audio_codec, "aac"))))) error = AVERROR_INVALIDDATA; + ctx->error = error < 0 ? -1 : 0; + if (ctx->error) DEBUG_ERROR(DEBUG_CATEGORY_VIDEO, "video preparation failed source=%s destination=%s error=%s", + ctx->source, ctx->destination, vc_errstr(error)); + vc_progress_set(ctx->error ? 0 : 1000); +} + +/* Только completion публикует файл: отменённый worker не заменяет прежний destination. */ +static void vc_done_trampoline(void* raw, int error) { + struct vc_ctx* ctx = raw; + if (!error) error = ctx->error; + if (!error && vc_file_publish(&ctx->file, ctx->source, ctx->destination)) error = -1; + vc_file_discard(&ctx->file); + vc_progress_set(-1); + vc_active_set(0); + DEBUG_DEBUG(DEBUG_CATEGORY_VIDEO, "video completion destination=%s error=%d", ctx->destination, error); + ctx->done(ctx->arg, error, error ? NULL : &ctx->result); + u_free(ctx); +} + +int video_transcode_start(struct media_async* owner, struct UASYNC* ua, const char* src, const char* dst, + void (*done)(void*, int, const struct video_info*), void* arg) { + if (!owner || !ua || !src || !dst || !*src || !*dst || !done || + strlen(src) >= VIDEO_PATH_MAX || strlen(dst) >= VIDEO_PATH_MAX) { + DEBUG_ERROR(DEBUG_CATEGORY_VIDEO, "video start invalid owner/loop/callback/path"); return -1; + } + struct vc_ctx* ctx = u_calloc(1, sizeof(*ctx)); + if (!ctx) { DEBUG_ERROR(DEBUG_CATEGORY_VIDEO, "video context allocation failed"); return -1; } + int expected = 0; + if (!__atomic_compare_exchange_n(&g_transcode_active, &expected, 1, 0, __ATOMIC_ACQ_REL, __ATOMIC_ACQUIRE)) { + DEBUG_WARN(DEBUG_CATEGORY_VIDEO, "video preparation already active"); u_free(ctx); return -1; + } + memcpy(ctx->source, src, strlen(src) + 1); memcpy(ctx->destination, dst, strlen(dst) + 1); + ctx->done = done; ctx->arg = arg; ctx->audio.index = -1; + vc_progress_set(0); + media_async_submit(owner, ua, vc_thread_entry, ctx, vc_done_trampoline, ctx); + return 0; /* submit мог синхронно вызвать done и освободить ctx. */ +} + +/* Получить первый доступный кадр, включая отложенные кадры после EOF контейнера. */ +static int vc_first_frame(AVFormatContext* format, AVCodecContext* decoder, int index, AVFrame* frame) { + AVPacket* packet = av_packet_alloc(); + if (!packet) return AVERROR(ENOMEM); + int error, draining = 0; + for (;;) { + error = av_read_frame(format, packet); + if (error == AVERROR_EOF) draining = 1; + else if (error < 0) break; + else if (packet->stream_index != index) { av_packet_unref(packet); continue; } + error = avcodec_send_packet(decoder, draining ? NULL : packet); + if (error == AVERROR(EAGAIN)) { + error = avcodec_receive_frame(decoder, frame); + if (error >= 0) break; + if (error == AVERROR(EAGAIN)) error = AVERROR_INVALIDDATA; + break; + } + av_packet_unref(packet); + if (error < 0) break; + error = avcodec_receive_frame(decoder, frame); + if (error >= 0) break; + if (error != AVERROR(EAGAIN) || draining) break; + } + av_packet_free(&packet); + return error; +} + +int video_extract_thumbnail(const char* src, const char* dst, int max_w) { + if (vc_paths(src, dst)) return -1; + if (max_w <= 0) max_w = 320; + if (max_w < 2) { DEBUG_ERROR(DEBUG_CATEGORY_VIDEO, "video thumbnail width must be >=2"); return -1; } + video_ffmpeg_init(); + struct vc_file output = {0}; + AVFormatContext* format = NULL; + AVCodecContext* decoder = NULL; AVCodecContext* encoder = NULL; + AVFrame* input = NULL; AVFrame* frame = NULL; AVPacket* packet = NULL; + struct SwsContext* scale = NULL; + const char* phase = "open input"; + int error = avformat_open_input(&format, src, NULL, NULL); + if (error < 0) goto done; + phase = "read stream info"; error = avformat_find_stream_info(format, NULL); + if (error < 0) goto done; + phase = "select stream"; + int index = vc_stream_index(format, AVMEDIA_TYPE_VIDEO); + if (index < 0) { error = index; goto done; } + const AVCodec* codec = avcodec_find_decoder(format->streams[index]->codecpar->codec_id); + if (!codec) { error = AVERROR_DECODER_NOT_FOUND; goto done; } + decoder = avcodec_alloc_context3(codec); input = av_frame_alloc(); + if (!decoder || !input) { error = AVERROR(ENOMEM); goto done; } + phase = "open decoder"; error = avcodec_parameters_to_context(decoder, format->streams[index]->codecpar); + if (error >= 0) error = avcodec_open2(decoder, codec, NULL); + if (error < 0) goto done; + phase = "decode first frame"; error = vc_first_frame(format, decoder, index, input); + if (error < 0) goto done; + if (input->width <= 0 || input->height <= 0) { error = AVERROR_INVALIDDATA; goto done; } + int width = FFMIN(input->width, max_w); + int height = (int)((int64_t)input->height * width / input->width); + width = FFMAX(2, width & ~1); height = FFMAX(2, height & ~1); + phase = "open JPEG encoder"; codec = avcodec_find_encoder(AV_CODEC_ID_MJPEG); + if (!codec) { error = AVERROR_ENCODER_NOT_FOUND; goto done; } + encoder = avcodec_alloc_context3(codec); frame = av_frame_alloc(); packet = av_packet_alloc(); + if (!encoder || !frame || !packet) { error = AVERROR(ENOMEM); goto done; } + encoder->width = width; encoder->height = height; encoder->pix_fmt = AV_PIX_FMT_YUVJ420P; + encoder->time_base = (AVRational){1, 25}; encoder->thread_count = 1; + error = avcodec_open2(encoder, codec, NULL); + if (error < 0) goto done; + frame->width = width; frame->height = height; frame->format = encoder->pix_fmt; frame->pts = 0; + phase = "scale JPEG"; error = av_frame_get_buffer(frame, 0); + if (error < 0) goto done; + scale = sws_getContext(input->width, input->height, input->format, width, height, frame->format, + SWS_BILINEAR, NULL, NULL, NULL); + if (!scale) { error = AVERROR(ENOMEM); goto done; } + if (sws_scale(scale, (const uint8_t* const*)input->data, input->linesize, 0, input->height, + frame->data, frame->linesize) != height) { error = AVERROR_INVALIDDATA; goto done; } + phase = "encode JPEG"; error = avcodec_send_frame(encoder, frame); + if (error >= 0) error = avcodec_receive_packet(encoder, packet); + if (error == AVERROR(EAGAIN)) { + error = avcodec_send_frame(encoder, NULL); + if (error >= 0) error = avcodec_receive_packet(encoder, packet); + } + if (error < 0) goto done; + phase = "write JPEG"; + if (vc_file_open(&output, dst)) { error = AVERROR(EIO); goto done; } + if (fwrite(packet->data, 1, packet->size, output.file) != (size_t)packet->size) { + error = AVERROR(errno ? errno : EIO); goto done; + } + if (vc_file_finish(&output)) { error = AVERROR(EIO); goto done; } + phase = "publish JPEG"; + error = vc_file_publish(&output, src, dst) ? AVERROR(EIO) : 0; +done: + if (error < 0) DEBUG_ERROR(DEBUG_CATEGORY_VIDEO, "video thumbnail failed phase=%s source=%s destination=%s error=%s", + phase, src, dst, vc_errstr(error)); + vc_file_discard(&output); + sws_freeContext(scale); av_frame_free(&input); av_frame_free(&frame); av_packet_free(&packet); + avcodec_free_context(&decoder); avcodec_free_context(&encoder); avformat_close_input(&format); + return error < 0 ? -1 : 0; } #endif /* HAVE_FFMPEG */