mirror of
https://github.com/lifegpc/eh-downloader.git
synced 2026-06-06 05:38:44 +08:00
Add thumbnail support via ffmpeg API (incomplete
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -5,3 +5,4 @@ downloads/
|
||||
utt.lock
|
||||
thumbnails/
|
||||
_fresh/
|
||||
lib/
|
||||
|
||||
3
.gitmodules
vendored
Normal file
3
.gitmodules
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
[submodule "extensions/utils"]
|
||||
path = extensions/utils
|
||||
url = https://github.com/lifegpc/c-utils
|
||||
@@ -19,7 +19,8 @@
|
||||
"config.json",
|
||||
"static/sw.js",
|
||||
"static/sw.meta.json",
|
||||
"_fresh"
|
||||
"_fresh",
|
||||
"extensions/build"
|
||||
]
|
||||
},
|
||||
"compilerOptions": {
|
||||
|
||||
1
extensions/.gitignore
vendored
Normal file
1
extensions/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
build
|
||||
9
extensions/CMakeLists.txt
Normal file
9
extensions/CMakeLists.txt
Normal file
@@ -0,0 +1,9 @@
|
||||
cmake_minimum_required(VERSION 3.18)
|
||||
|
||||
project(extensions)
|
||||
|
||||
set(ENABLE_ICONV OFF CACHE BOOL "Libiconv is not needed.")
|
||||
add_subdirectory(utils)
|
||||
include_directories("${CMAKE_CURRENT_SOURCE_DIR}/utils")
|
||||
|
||||
add_subdirectory(thumbnail)
|
||||
33
extensions/cmake/FindAVCODEC.cmake
Normal file
33
extensions/cmake/FindAVCODEC.cmake
Normal file
@@ -0,0 +1,33 @@
|
||||
find_package(PkgConfig)
|
||||
if (PkgConfig_FOUND)
|
||||
pkg_check_modules(PC_AVCODEC QUIET IMPORTED_TARGET GLOBAL libavcodec)
|
||||
endif()
|
||||
|
||||
if (PC_AVCODEC_FOUND)
|
||||
set(AVCODEC_FOUND TRUE)
|
||||
set(AVCODEC_VERSION ${PC_AVCODEC_VERSION})
|
||||
set(AVCODEC_VERSION_STRING ${PC_AVCODEC_STRING})
|
||||
set(AVCODEC_LIBRARYS ${PC_AVCODEC_LIBRARIES})
|
||||
if (USE_STATIC_LIBS)
|
||||
set(AVCODEC_INCLUDE_DIRS ${PC_AVCODEC_STATIC_INCLUDE_DIRS})
|
||||
else()
|
||||
set(AVCODEC_INCLUDE_DIRS ${PC_AVCODEC_INCLUDE_DIRS})
|
||||
endif()
|
||||
if (NOT AVCODEC_INCLUDE_DIRS)
|
||||
find_path(AVCODEC_INCLUDE_DIRS NAMES libavcodec/avcodec.h)
|
||||
endif()
|
||||
if (NOT TARGET AVCODEC::AVCODEC)
|
||||
add_library(AVCODEC::AVCODEC ALIAS PkgConfig::PC_AVCODEC)
|
||||
endif()
|
||||
else()
|
||||
message(FATAL_ERROR "failed.")
|
||||
endif()
|
||||
|
||||
include(FindPackageHandleStandardArgs)
|
||||
find_package_handle_standard_args(AVCODEC
|
||||
FOUND_VAR AVCODEC_FOUND
|
||||
REQUIRED_VARS
|
||||
AVCODEC_LIBRARYS
|
||||
AVCODEC_INCLUDE_DIRS
|
||||
VERSION_VAR AVCODEC_VERSION
|
||||
)
|
||||
33
extensions/cmake/FindAVFORMAT.cmake
Normal file
33
extensions/cmake/FindAVFORMAT.cmake
Normal file
@@ -0,0 +1,33 @@
|
||||
find_package(PkgConfig)
|
||||
if (PkgConfig_FOUND)
|
||||
pkg_check_modules(PC_AVFORMAT QUIET IMPORTED_TARGET GLOBAL libavformat)
|
||||
endif()
|
||||
|
||||
if (PC_AVFORMAT_FOUND)
|
||||
set(AVFORMAT_FOUND TRUE)
|
||||
set(AVFORMAT_VERSION ${PC_AVFORMAT_VERSION})
|
||||
set(AVFORMAT_VERSION_STRING ${PC_AVFORMAT_STRING})
|
||||
set(AVFORMAT_LIBRARYS ${PC_AVFORMAT_LIBRARIES})
|
||||
if (USE_STATIC_LIBS)
|
||||
set(AVFORMAT_INCLUDE_DIRS ${PC_AVFORMAT_STATIC_INCLUDE_DIRS})
|
||||
else()
|
||||
set(AVFORMAT_INCLUDE_DIRS ${PC_AVFORMAT_INCLUDE_DIRS})
|
||||
endif()
|
||||
if (NOT AVFORMAT_INCLUDE_DIRS)
|
||||
find_path(AVFORMAT_INCLUDE_DIRS NAMES libavformat/avformat.h)
|
||||
endif()
|
||||
if (NOT TARGET AVFORMAT::AVFORMAT)
|
||||
add_library(AVFORMAT::AVFORMAT ALIAS PkgConfig::PC_AVFORMAT)
|
||||
endif()
|
||||
else()
|
||||
message(FATAL_ERROR "failed.")
|
||||
endif()
|
||||
|
||||
include(FindPackageHandleStandardArgs)
|
||||
find_package_handle_standard_args(AVFORMAT
|
||||
FOUND_VAR AVFORMAT_FOUND
|
||||
REQUIRED_VARS
|
||||
AVFORMAT_LIBRARYS
|
||||
AVFORMAT_INCLUDE_DIRS
|
||||
VERSION_VAR AVFORMAT_VERSION
|
||||
)
|
||||
33
extensions/cmake/FindAVUTIL.cmake
Normal file
33
extensions/cmake/FindAVUTIL.cmake
Normal file
@@ -0,0 +1,33 @@
|
||||
find_package(PkgConfig)
|
||||
if (PkgConfig_FOUND)
|
||||
pkg_check_modules(PC_AVUTIL QUIET IMPORTED_TARGET GLOBAL libavutil)
|
||||
endif()
|
||||
|
||||
if (PC_AVUTIL_FOUND)
|
||||
set(AVUTIL_FOUND TRUE)
|
||||
set(AVUTIL_VERSION ${PC_AVUTIL_VERSION})
|
||||
set(AVUTIL_VERSION_STRING ${PC_AVUTIL_STRING})
|
||||
set(AVUTIL_LIBRARYS ${PC_AVUTIL_LIBRARIES})
|
||||
if (USE_STATIC_LIBS)
|
||||
set(AVUTIL_INCLUDE_DIRS ${PC_AVUTIL_STATIC_INCLUDE_DIRS})
|
||||
else()
|
||||
set(AVUTIL_INCLUDE_DIRS ${PC_AVUTIL_INCLUDE_DIRS})
|
||||
endif()
|
||||
if (NOT AVUTIL_INCLUDE_DIRS)
|
||||
find_path(AVUTIL_INCLUDE_DIRS NAMES libavutil/avutil.h)
|
||||
endif()
|
||||
if (NOT TARGET AVUTIL::AVUTIL)
|
||||
add_library(AVUTIL::AVUTIL ALIAS PkgConfig::PC_AVUTIL)
|
||||
endif()
|
||||
else()
|
||||
message(FATAL_ERROR "failed.")
|
||||
endif()
|
||||
|
||||
include(FindPackageHandleStandardArgs)
|
||||
find_package_handle_standard_args(AVUTIL
|
||||
FOUND_VAR AVUTIL_FOUND
|
||||
REQUIRED_VARS
|
||||
AVUTIL_LIBRARYS
|
||||
AVUTIL_INCLUDE_DIRS
|
||||
VERSION_VAR AVUTIL_VERSION
|
||||
)
|
||||
32
extensions/cmake/FindSWSCALE.cmake
Normal file
32
extensions/cmake/FindSWSCALE.cmake
Normal file
@@ -0,0 +1,32 @@
|
||||
find_package(PkgConfig)
|
||||
if (PkgConfig_FOUND)
|
||||
pkg_check_modules(PC_SWSCALE QUIET IMPORTED_TARGET GLOBAL libswscale)
|
||||
endif()
|
||||
|
||||
if (PC_SWSCALE_FOUND)
|
||||
set(SWSCALE_FOUND TRUE)
|
||||
set(SWSCALE_VERSION ${PC_SWSCALE_VERSION})
|
||||
set(SWSCALE_VERSION_STRING ${PC_SWSCALE_STRING})
|
||||
set(SWSCALE_LIBRARYS ${PC_SWSCALE_LIBRARIES})
|
||||
if (USE_STATIC_LIBS)
|
||||
set(SWSCALE_INCLUDE_DIRS ${PC_SWSCALE_STATIC_INCLUDE_DIRS})
|
||||
else()
|
||||
set(SWSCALE_INCLUDE_DIRS ${PC_SWSCALE_INCLUDE_DIRS})
|
||||
endif()
|
||||
if (NOT SWSCALE_INCLUDE_DIRS)
|
||||
find_path(SWSCALE_INCLUDE_DIRS NAMES libswscale/swscale.h)
|
||||
endif()
|
||||
if (NOT TARGET SWSCALE::SWSCALE)
|
||||
add_library(SWSCALE::SWSCALE ALIAS PkgConfig::PC_SWSCALE)
|
||||
endif()
|
||||
else()
|
||||
message(FATAL_ERROR "failed.")
|
||||
endif()
|
||||
|
||||
include(FindPackageHandleStandardArgs)
|
||||
find_package_handle_standard_args(SWSCALE
|
||||
FOUND_VAR SWSCALE_FOUND
|
||||
REQUIRED_VARS
|
||||
SWSCALE_LIBRARYS
|
||||
VERSION_VAR SWSCALE_VERSION
|
||||
)
|
||||
28
extensions/thumbnail/CMakeLists.txt
Normal file
28
extensions/thumbnail/CMakeLists.txt
Normal file
@@ -0,0 +1,28 @@
|
||||
cmake_minimum_required(VERSION 3.18)
|
||||
list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/../cmake")
|
||||
|
||||
project(thumbnail)
|
||||
if (MSVC)
|
||||
add_compile_options(/utf-8)
|
||||
endif()
|
||||
|
||||
if (NOT TARGET utils)
|
||||
add_subdirectory("${CMAKE_CURRENT_SOURCE_DIR}/../utils" "${CMAKE_BINARY_DIR}/utils")
|
||||
include_directories("${CMAKE_CURRENT_SOURCE_DIR}/../utils")
|
||||
endif()
|
||||
|
||||
find_package(AVFORMAT 58 REQUIRED)
|
||||
find_package(AVCODEC 58 REQUIRED)
|
||||
find_package(AVUTIL 56 REQUIRED)
|
||||
find_package(SWSCALE 5 REQUIRED)
|
||||
|
||||
include(GNUInstallDirs)
|
||||
|
||||
add_library(thumbnail SHARED thumbnail.h thumbnail.c)
|
||||
target_link_libraries(thumbnail AVFORMAT::AVFORMAT)
|
||||
target_link_libraries(thumbnail AVCODEC::AVCODEC)
|
||||
target_link_libraries(thumbnail AVUTIL::AVUTIL)
|
||||
target_link_libraries(thumbnail SWSCALE::SWSCALE)
|
||||
target_link_libraries(thumbnail utils)
|
||||
|
||||
install(TARGETS thumbnail)
|
||||
329
extensions/thumbnail/thumbnail.c
Normal file
329
extensions/thumbnail/thumbnail.c
Normal file
@@ -0,0 +1,329 @@
|
||||
#include "thumbnail.h"
|
||||
#include "cfileop.h"
|
||||
#include "libavformat/avformat.h"
|
||||
#include "libavcodec/avcodec.h"
|
||||
#include "libswscale/swscale.h"
|
||||
#include <string.h>
|
||||
|
||||
PUBLIC_API void thumbnail_fferror(int e, char* buf, size_t bufsize) {
|
||||
if (!buf) return;
|
||||
av_make_error_string(buf, bufsize, e);
|
||||
}
|
||||
|
||||
int thumbnail_convert_frame(THUMBNAIL_ERROR* err, AVFrame* ifr, AVFrame** ofr, AVCodecContext* occ, THUMBNAIL_METHOD method) {
|
||||
if (!err || !ifr || !ofr) return 1;
|
||||
int re = 0;
|
||||
struct SwsContext* sws = NULL;
|
||||
AVFrame* fr = NULL, * fr2 = NULL;
|
||||
if (!(fr = av_frame_alloc())) {
|
||||
err->e = THUMBNAIL_OOM;
|
||||
re = 1;
|
||||
goto end;
|
||||
}
|
||||
int theight = ifr->height * occ->width / ifr->width;
|
||||
char simple_way = 0;
|
||||
if (occ->height == theight || method == THUMBNAIL_FILL) {
|
||||
simple_way = 1;
|
||||
}
|
||||
if (simple_way) {
|
||||
fr->width = occ->width;
|
||||
fr->height = occ->height;
|
||||
fr->format = occ->pix_fmt;
|
||||
fr->sample_aspect_ratio = ifr->sample_aspect_ratio;
|
||||
}
|
||||
if ((err->fferr = av_frame_get_buffer(fr, 0)) < 0) {
|
||||
err->e = THUMBNAIL_FFMPEG_ERROR;
|
||||
re = 1;
|
||||
av_log(NULL, AV_LOG_ERROR, "Failed to get buffer for output frame: %s\n", av_err2str(err->fferr));
|
||||
goto end;
|
||||
}
|
||||
if ((err->fferr = av_frame_make_writable(fr)) < 0) {
|
||||
err->e = THUMBNAIL_FFMPEG_ERROR;
|
||||
re = 1;
|
||||
av_log(NULL, AV_LOG_ERROR, "Failed to make writeable for output frame: %s\n", av_err2str(err->fferr));
|
||||
goto end;
|
||||
}
|
||||
if (simple_way) {
|
||||
if (!(sws = sws_getContext(ifr->width, ifr->height, (enum AVPixelFormat)ifr->format, fr->width, fr->height, (enum AVPixelFormat)fr->format, SWS_BILINEAR, NULL, NULL, NULL))) {
|
||||
err->e = THUMBNAIL_UNABLE_SCALE;
|
||||
re = 1;
|
||||
goto end;
|
||||
}
|
||||
if ((err->fferr = sws_scale(sws, (const uint8_t* const*)ifr->data, ifr->linesize, 0, ifr->height, fr->data, fr->linesize)) < 0) {
|
||||
err->e = THUMBNAIL_UNABLE_SCALE;
|
||||
re = 1;
|
||||
goto end;
|
||||
}
|
||||
}
|
||||
end:
|
||||
if (re == 1 && fr) av_frame_free(&fr);
|
||||
else if (re == 0) *ofr = fr;
|
||||
if (fr2) av_frame_free(&fr2);
|
||||
if (sws) sws_freeContext(sws);
|
||||
return re;
|
||||
}
|
||||
|
||||
int thumbnail_encode_video(THUMBNAIL_ERROR* err, AVFrame* ofr, AVFormatContext* oc, AVCodecContext* occ, char* writed_data) {
|
||||
if (!err || !oc || !occ || !writed_data) return 1;
|
||||
int re = 0;
|
||||
AVPacket* pkt = av_packet_alloc();
|
||||
if (!pkt) {
|
||||
err->e = THUMBNAIL_OOM;
|
||||
re = 1;
|
||||
goto end;
|
||||
}
|
||||
*writed_data = 0;
|
||||
if (ofr) {
|
||||
ofr->pts = 0;
|
||||
ofr->pkt_dts = 0;
|
||||
}
|
||||
if ((err->fferr = avcodec_send_frame(occ, ofr)) < 0) {
|
||||
if (err->fferr == AVERROR_EOF) {
|
||||
err->fferr = 0;
|
||||
} else {
|
||||
av_log(NULL, AV_LOG_ERROR, "Failed to send frame to encoder: %s\n", av_err2str(err->fferr));
|
||||
err->e = THUMBNAIL_FFMPEG_ERROR;
|
||||
re = 1;
|
||||
goto end;
|
||||
}
|
||||
}
|
||||
err->fferr = avcodec_receive_packet(occ, pkt);
|
||||
if (err->fferr >= 0) {
|
||||
*writed_data = 1;
|
||||
} else if (err->fferr == AVERROR_EOF || err->fferr == AVERROR(EAGAIN)) {
|
||||
err->fferr = 0;
|
||||
goto end;
|
||||
} else {
|
||||
av_log(NULL, AV_LOG_ERROR, "Failed to recive data from encoder: %s\n", av_err2str(err->fferr));
|
||||
err->e = THUMBNAIL_FFMPEG_ERROR;
|
||||
re = 1;
|
||||
goto end;
|
||||
}
|
||||
if (*writed_data && pkt) {
|
||||
pkt->stream_index = 0;
|
||||
if ((err->fferr = av_write_frame(oc, pkt)) < 0) {
|
||||
err->e = THUMBNAIL_FFMPEG_ERROR;
|
||||
av_log(NULL, AV_LOG_ERROR, "Failed to write data to muxer: %s\n", av_err2str(err->fferr));
|
||||
re = 1;
|
||||
goto end;
|
||||
}
|
||||
}
|
||||
end:
|
||||
if (pkt) av_packet_free(&pkt);
|
||||
return re;
|
||||
}
|
||||
|
||||
THUMBNAIL_ERROR gen_thumbnail(const char* src, const char* dest, int width, int height, THUMBNAIL_METHOD method) {
|
||||
THUMBNAIL_ERROR re = { THUMBNAIL_OK, 0 };
|
||||
AVFormatContext* ic = NULL, * oc = NULL;
|
||||
AVStream* is = NULL, * os = NULL;
|
||||
const AVCodec* input_codec = NULL, * output_codec = NULL;
|
||||
AVCodecContext* icc = NULL, * occ = NULL;
|
||||
AVPacket pkt;
|
||||
AVFrame* ifr = NULL, * ofr = NULL;
|
||||
if (method < THUMBNAIL_COVER || method > THUMBNAIL_FILL) {
|
||||
re.e = THUMBNAIL_UNKNOWN_METHOD;
|
||||
goto end;
|
||||
}
|
||||
if (!src || !dest) {
|
||||
re.e = THUMBNAIL_NULL_POINTER;
|
||||
goto end;
|
||||
}
|
||||
if (fileop_exists(dest)) {
|
||||
if (!fileop_remove(dest)) {
|
||||
re.e = THUMBNAIL_REMOVE_OUTPUT_FILE_FAILED;
|
||||
goto end;
|
||||
}
|
||||
}
|
||||
if ((re.fferr = avformat_open_input(&ic, src, NULL, NULL)) < 0) {
|
||||
re.e = THUMBNAIL_FFMPEG_ERROR;
|
||||
av_log(NULL, AV_LOG_ERROR, "Failed to open file: %s\n", av_err2str(re.fferr));
|
||||
goto end;
|
||||
}
|
||||
if ((re.fferr = avformat_find_stream_info(ic, NULL)) < 0) {
|
||||
re.e = THUMBNAIL_FFMPEG_ERROR;
|
||||
av_log(NULL, AV_LOG_ERROR, "Failed to find stream info in file: %s\n", av_err2str(re.fferr));
|
||||
goto end;
|
||||
}
|
||||
for (unsigned int i = 0; i < ic->nb_streams; i++) {
|
||||
AVStream* s = ic->streams[i];
|
||||
if (s->codecpar && s->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) {
|
||||
is = s;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!is) {
|
||||
re.e = THUMBNAIL_NO_VIDEO_STREAM;
|
||||
goto end;
|
||||
}
|
||||
if ((re.fferr = avformat_alloc_output_context2(&oc, NULL, "mjpeg", dest)) < 0) {
|
||||
av_log(NULL, AV_LOG_ERROR, "Failed to allocate output context: %s\n", av_err2str(re.fferr));
|
||||
re.e = THUMBNAIL_FFMPEG_ERROR;
|
||||
goto end;
|
||||
}
|
||||
if (!(input_codec = avcodec_find_decoder(is->codecpar->codec_id))) {
|
||||
re.e = THUMBNAIL_NO_DECODER;
|
||||
goto end;
|
||||
}
|
||||
if (!(icc = avcodec_alloc_context3(input_codec))) {
|
||||
re.e = THUMBNAIL_OOM;
|
||||
goto end;
|
||||
}
|
||||
if ((re.fferr = avcodec_parameters_to_context(icc, is->codecpar)) < 0) {
|
||||
av_log(NULL, AV_LOG_ERROR, "Failed to copy decode parameters: %s\n", av_err2str(re.fferr));
|
||||
re.e = THUMBNAIL_FFMPEG_ERROR;
|
||||
goto end;
|
||||
}
|
||||
if ((re.fferr = avcodec_open2(icc, input_codec, NULL)) < 0) {
|
||||
av_log(NULL, AV_LOG_ERROR, "Failed to open decoder: %s\n", av_err2str(re.fferr));
|
||||
re.e = THUMBNAIL_FFMPEG_ERROR;
|
||||
goto end;
|
||||
}
|
||||
output_codec = avcodec_find_encoder(AV_CODEC_ID_MJPEG);
|
||||
if (!output_codec) {
|
||||
re.e = THUMBNAIL_NO_ENCODER;
|
||||
goto end;
|
||||
}
|
||||
if (!(occ = avcodec_alloc_context3(output_codec))) {
|
||||
re.e = THUMBNAIL_OOM;
|
||||
goto end;
|
||||
}
|
||||
occ->width = width;
|
||||
occ->height = height;
|
||||
occ->pix_fmt = AV_PIX_FMT_YUVJ420P;
|
||||
occ->sample_aspect_ratio = icc->sample_aspect_ratio;
|
||||
occ->time_base = AV_TIME_BASE_Q;
|
||||
occ->color_range = AVCOL_RANGE_JPEG;
|
||||
if ((re.fferr = avcodec_open2(occ, output_codec, NULL)) < 0) {
|
||||
av_log(NULL, AV_LOG_ERROR, "Failed to open encoder: %s\n", av_err2str(re.fferr));
|
||||
re.e = THUMBNAIL_FFMPEG_ERROR;
|
||||
goto end;
|
||||
}
|
||||
if (!(os = avformat_new_stream(oc, NULL))) {
|
||||
re.e = THUMBNAIL_OOM;
|
||||
goto end;
|
||||
}
|
||||
if ((re.fferr = avcodec_parameters_from_context(os->codecpar, occ)) < 0) {
|
||||
av_log(NULL, AV_LOG_ERROR, "Failed to copy encoder's params to muxer: %s\n", av_err2str(re.fferr));
|
||||
re.e = THUMBNAIL_FFMPEG_ERROR;
|
||||
goto end;
|
||||
}
|
||||
if (!(oc->oformat->flags & AVFMT_NOFILE)) {
|
||||
if ((re.fferr = avio_open(&oc->pb, dest, AVIO_FLAG_WRITE)) < 0) {
|
||||
av_log(NULL, AV_LOG_ERROR, "Failed to open output file: %s\n", av_err2str(re.fferr));
|
||||
re.e = THUMBNAIL_FFMPEG_ERROR;
|
||||
goto end;
|
||||
}
|
||||
}
|
||||
if ((re.fferr = avformat_write_header(oc, NULL)) < 0) {
|
||||
av_log(NULL, AV_LOG_ERROR, "Failed to write headers to file: %s\n", av_err2str(re.fferr));
|
||||
re.e = THUMBNAIL_FFMPEG_ERROR;
|
||||
goto end;
|
||||
}
|
||||
while (1) {
|
||||
if ((re.fferr = av_read_frame(ic, &pkt)) < 0) {
|
||||
av_log(NULL, AV_LOG_ERROR, "Failed to read frames from file: %s\n", av_err2str(re.fferr));
|
||||
re.e = THUMBNAIL_FFMPEG_ERROR;
|
||||
goto end;
|
||||
}
|
||||
if (pkt.data == NULL) {
|
||||
av_packet_unref(&pkt);
|
||||
av_log(NULL, AV_LOG_ERROR, "No data find in frames: %s\n", av_err2str(re.fferr));
|
||||
re.e = THUMBNAIL_FFMPEG_ERROR;
|
||||
goto end;
|
||||
}
|
||||
if (pkt.stream_index != is->index) {
|
||||
av_packet_unref(&pkt);
|
||||
continue;
|
||||
}
|
||||
if (!(ifr = av_frame_alloc())) {
|
||||
av_packet_unref(&pkt);
|
||||
re.e = THUMBNAIL_OOM;
|
||||
goto end;
|
||||
}
|
||||
if ((re.fferr = avcodec_send_packet(icc, &pkt)) < 0) {
|
||||
av_packet_unref(&pkt);
|
||||
av_log(NULL, AV_LOG_ERROR, "Failed to send packet to decoder: %s\n", av_err2str(re.fferr));
|
||||
re.e = THUMBNAIL_FFMPEG_ERROR;
|
||||
goto end;
|
||||
}
|
||||
if ((re.fferr = avcodec_receive_frame(icc, ifr)) < 0) {
|
||||
if (re.fferr == AVERROR(EAGAIN)) {
|
||||
av_packet_unref(&pkt);
|
||||
re.fferr = 0;
|
||||
continue;
|
||||
}
|
||||
av_packet_unref(&pkt);
|
||||
av_log(NULL, AV_LOG_ERROR, "Failed to receive frame from decoder: %s\n", av_err2str(re.fferr));
|
||||
re.e = THUMBNAIL_FFMPEG_ERROR;
|
||||
goto end;
|
||||
}
|
||||
if (thumbnail_convert_frame(&re, ifr, &ofr, occ, method)) {
|
||||
av_packet_unref(&pkt);
|
||||
re.e = THUMBNAIL_FFMPEG_ERROR;
|
||||
goto end;
|
||||
}
|
||||
char writed = 0;
|
||||
if (thumbnail_encode_video(&re, ofr, oc, occ, &writed)) {
|
||||
av_packet_unref(&pkt);
|
||||
goto end;
|
||||
}
|
||||
while (1) {
|
||||
if (thumbnail_encode_video(&re, NULL, oc, occ, &writed)) {
|
||||
av_packet_unref(&pkt);
|
||||
goto end;
|
||||
}
|
||||
if (!writed) break;
|
||||
}
|
||||
av_packet_unref(&pkt);
|
||||
break;
|
||||
}
|
||||
av_write_trailer(oc);
|
||||
end:
|
||||
if (ifr) av_frame_free(&ifr);
|
||||
if (ofr) av_frame_free(&ofr);
|
||||
if (icc) avcodec_free_context(&icc);
|
||||
if (occ) avcodec_free_context(&occ);
|
||||
if (oc) {
|
||||
if (!(oc->oformat->flags & AVFMT_NOFILE)) avio_closep(&oc->pb);
|
||||
avformat_free_context(oc);
|
||||
}
|
||||
if (ic) avformat_close_input(&ic);
|
||||
return re;
|
||||
}
|
||||
|
||||
const char* thumbnail_berror(THUMBNAIL_ERROR_E e) {
|
||||
switch (e) {
|
||||
case THUMBNAIL_OK:
|
||||
return "OK";
|
||||
case THUMBNAIL_FFMPEG_ERROR:
|
||||
return "A error occured in ffmpeg code.";
|
||||
case THUMBNAIL_NULL_POINTER:
|
||||
return "Arguments have null pointers.";
|
||||
case THUMBNAIL_REMOVE_OUTPUT_FILE_FAILED:
|
||||
return "Can not remove output file.";
|
||||
case THUMBNAIL_NO_VIDEO_STREAM:
|
||||
return "Can not find video stream in source file.";
|
||||
case THUMBNAIL_OOM:
|
||||
return "Out of memory.";
|
||||
case THUMBNAIL_NO_DECODER:
|
||||
return "No available decoder.";
|
||||
case THUMBNAIL_NO_ENCODER:
|
||||
return "No available encoder.";
|
||||
case THUMBNAIL_UNABLE_SCALE:
|
||||
return "Unable to scale image.";
|
||||
default:
|
||||
return "Unknown error.";
|
||||
}
|
||||
}
|
||||
|
||||
void thumbnail_error(THUMBNAIL_ERROR e, char* buf, size_t bufsize) {
|
||||
switch (e.e) {
|
||||
case THUMBNAIL_FFMPEG_ERROR:
|
||||
thumbnail_fferror(e.fferr, buf, bufsize);
|
||||
break;
|
||||
default:
|
||||
strncpy(buf, thumbnail_berror(e.e), bufsize);
|
||||
break;
|
||||
}
|
||||
}
|
||||
31
extensions/thumbnail/thumbnail.h
Normal file
31
extensions/thumbnail/thumbnail.h
Normal file
@@ -0,0 +1,31 @@
|
||||
#pragma once
|
||||
|
||||
#include <stddef.h>
|
||||
#define PUBLIC_API __declspec(dllexport)
|
||||
|
||||
typedef enum THUMBNAIL_ERROR_E {
|
||||
THUMBNAIL_OK,
|
||||
THUMBNAIL_NULL_POINTER,
|
||||
THUMBNAIL_REMOVE_OUTPUT_FILE_FAILED,
|
||||
THUMBNAIL_FFMPEG_ERROR,
|
||||
THUMBNAIL_NO_VIDEO_STREAM,
|
||||
THUMBNAIL_UNKNOWN_METHOD,
|
||||
THUMBNAIL_NO_DECODER,
|
||||
THUMBNAIL_OOM,
|
||||
THUMBNAIL_NO_ENCODER,
|
||||
THUMBNAIL_UNABLE_SCALE,
|
||||
} THUMBNAIL_ERROR_E;
|
||||
|
||||
typedef struct THUMBNAIL_ERROR {
|
||||
THUMBNAIL_ERROR_E e;
|
||||
int fferr;
|
||||
}THUMBNAIL_ERROR;
|
||||
|
||||
typedef enum THUMBNAIL_METHOD {
|
||||
THUMBNAIL_COVER = 1,
|
||||
THUMBNAIL_CONTAIN,
|
||||
THUMBNAIL_FILL,
|
||||
} THUMBNAIL_METHOD;
|
||||
|
||||
PUBLIC_API THUMBNAIL_ERROR gen_thumbnail(const char* src, const char* dest, int width, int height, THUMBNAIL_METHOD method);
|
||||
PUBLIC_API void thumbnail_error(THUMBNAIL_ERROR e, char* buf, size_t bufsize);
|
||||
1
extensions/utils
Submodule
1
extensions/utils
Submodule
Submodule extensions/utils added at 40e3df54a9
@@ -27,6 +27,7 @@
|
||||
"@material/web/": "https://unpkg.lifegpc.workers.dev/@material/[email protected]/",
|
||||
"@lit-labs/react/": "https://esm.sh/@lit-labs/[email protected]/",
|
||||
"bootstrap/": "https://esm.sh/[email protected]/",
|
||||
"filesize": "https://esm.sh/[email protected]"
|
||||
"filesize": "https://esm.sh/[email protected]",
|
||||
"pwn/": "https://deno.land/x/[email protected]/"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,11 @@ import { Handlers } from "$fresh/server.ts";
|
||||
import { exists } from "std/fs/exists.ts";
|
||||
import { get_task_manager } from "../../../server.ts";
|
||||
import { parse_bool, parse_int } from "../../../server/parse_form.ts";
|
||||
import { generate_filename, ThumbnailConfig } from "../../../thumbnail/base.ts";
|
||||
import {
|
||||
generate_filename,
|
||||
ThumbnailConfig,
|
||||
ThumbnailGenMethod,
|
||||
} from "../../../thumbnail/base.ts";
|
||||
import { sure_dir } from "../../../utils.ts";
|
||||
import { ThumbnailMethod } from "../../../config.ts";
|
||||
import { fb_generate_thumbnail } from "../../../thumbnail/ffmpeg_binary.ts";
|
||||
@@ -35,7 +39,12 @@ export const handler: Handlers = {
|
||||
const height = await parse_int(u.searchParams.get("height"), null);
|
||||
const quality = await parse_int(u.searchParams.get("quality"), 1);
|
||||
const force = await parse_bool(u.searchParams.get("force"), false);
|
||||
const cfg: ThumbnailConfig = { width: 0, height: 0, quality };
|
||||
const cfg: ThumbnailConfig = {
|
||||
width: 0,
|
||||
height: 0,
|
||||
quality,
|
||||
method: ThumbnailGenMethod.Unknown,
|
||||
};
|
||||
if (width !== null && height !== null) {
|
||||
cfg.width = width;
|
||||
cfg.height = height;
|
||||
|
||||
@@ -2,10 +2,18 @@ import { join } from "std/path/mod.ts";
|
||||
import { filterFilename } from "../utils.ts";
|
||||
import type { EhFile } from "../db.ts";
|
||||
|
||||
export enum ThumbnailGenMethod {
|
||||
Unknown,
|
||||
Cover,
|
||||
Contain,
|
||||
Fill,
|
||||
}
|
||||
|
||||
export type ThumbnailConfig = {
|
||||
width: number;
|
||||
height: number;
|
||||
quality: number;
|
||||
method: ThumbnailGenMethod;
|
||||
};
|
||||
|
||||
export function generate_filename(
|
||||
@@ -13,10 +21,22 @@ export function generate_filename(
|
||||
f: EhFile,
|
||||
cfg: ThumbnailConfig,
|
||||
) {
|
||||
let method = "";
|
||||
switch (cfg.method) {
|
||||
case ThumbnailGenMethod.Cover:
|
||||
method = "-cover";
|
||||
break;
|
||||
case ThumbnailGenMethod.Contain:
|
||||
method = "-contain";
|
||||
break;
|
||||
case ThumbnailGenMethod.Fill:
|
||||
method = "-fill";
|
||||
break;
|
||||
}
|
||||
return join(
|
||||
base,
|
||||
filterFilename(
|
||||
`${f.id}-${f.token}-${cfg.width}x${cfg.height}-q${cfg.quality}.jpg`,
|
||||
`${f.id}-${f.token}-${cfg.width}x${cfg.height}-q${cfg.quality}${method}.jpg`,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
68
thumbnail/ffmpeg_api.ts
Normal file
68
thumbnail/ffmpeg_api.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
/// <reference lib="deno.unstable" />
|
||||
import { Struct } from "pwn/mod.ts";
|
||||
import { ThumbnailGenMethod } from "./base.ts";
|
||||
|
||||
let libSuffix = "";
|
||||
let libPrefix = "lib";
|
||||
switch (Deno.build.os) {
|
||||
case "windows":
|
||||
libSuffix = "dll";
|
||||
libPrefix = "";
|
||||
break;
|
||||
case "darwin":
|
||||
libSuffix = "dylib";
|
||||
break;
|
||||
default:
|
||||
libSuffix = "so";
|
||||
break;
|
||||
}
|
||||
|
||||
let libPath = import.meta.resolve(`../lib/${libPrefix}thumbnail.${libSuffix}`)
|
||||
.slice(7);
|
||||
if (Deno.build.os === "windows") {
|
||||
libPath = libPath.slice(1);
|
||||
}
|
||||
|
||||
const lib = Deno.dlopen(
|
||||
libPath,
|
||||
{
|
||||
"gen_thumbnail": {
|
||||
"parameters": ["buffer", "buffer", "i32", "i32", "i32"],
|
||||
"result": { struct: ["i32", "i32"] },
|
||||
"nonblocking": true,
|
||||
},
|
||||
"thumbnail_error": {
|
||||
"parameters": [{ struct: ["i32", "i32"] }, "buffer", "usize"],
|
||||
"result": "void",
|
||||
},
|
||||
} as const,
|
||||
);
|
||||
const _Result = new Struct({ e: "s32", fferr: "s32" });
|
||||
|
||||
function get_error(fferr: Uint8Array) {
|
||||
const u = new Uint8Array(64);
|
||||
lib.symbols.thumbnail_error(fferr, u, u.length);
|
||||
let len = u.findIndex((i) => i === 0);
|
||||
if (len === -1) len = u.length;
|
||||
return (new TextDecoder()).decode(u.slice(0, len));
|
||||
}
|
||||
|
||||
export async function gen_thumbnail(
|
||||
src: string,
|
||||
dest: string,
|
||||
width: number,
|
||||
height: number,
|
||||
method: ThumbnailGenMethod,
|
||||
) {
|
||||
const t = new TextEncoder();
|
||||
const ore = await lib.symbols.gen_thumbnail(
|
||||
t.encode(`${src}\0`),
|
||||
t.encode(`${dest}\0`),
|
||||
width,
|
||||
height,
|
||||
method,
|
||||
);
|
||||
const re = _Result.unpack(ore);
|
||||
if (re.e) return get_error(ore);
|
||||
return;
|
||||
}
|
||||
Reference in New Issue
Block a user