static void _magickPostVideoFrame(struct GBAAVStream* stream, struct GBAVideoRenderer* renderer) { struct ImageMagickGIFEncoder* encoder = (struct ImageMagickGIFEncoder*) stream; if (encoder->currentFrame % (encoder->frameskip + 1)) { ++encoder->currentFrame; return; } const uint8_t* pixels; unsigned stride; renderer->getPixels(renderer, &stride, (const void**) &pixels); size_t row; for (row = 0; row < VIDEO_VERTICAL_PIXELS; ++row) { memcpy(&encoder->frame[row * VIDEO_HORIZONTAL_PIXELS], &pixels[row * 4 * stride], VIDEO_HORIZONTAL_PIXELS * 4); } MagickConstituteImage(encoder->wand, VIDEO_HORIZONTAL_PIXELS, VIDEO_VERTICAL_PIXELS, "RGBP", CharPixel, encoder->frame); uint64_t ts = encoder->currentFrame; uint64_t nts = encoder->currentFrame + encoder->frameskip + 1; if (encoder->delayMs >= 0) { ts *= encoder->delayMs; nts *= encoder->delayMs; ts /= 10; nts /= 10; } else { ts *= VIDEO_TOTAL_LENGTH * 100; nts *= VIDEO_TOTAL_LENGTH * 100; ts /= GBA_ARM7TDMI_FREQUENCY; nts /= GBA_ARM7TDMI_FREQUENCY; } MagickSetImageDelay(encoder->wand, nts - ts); ++encoder->currentFrame; }
static int ngx_http_video_thumbextractor_get_thumb(ngx_http_video_thumbextractor_loc_conf_t *cf, ngx_http_video_thumbextractor_file_info_t *info, int64_t second, ngx_uint_t width, ngx_uint_t height, caddr_t *out_buffer, size_t *out_len, ngx_pool_t *temp_pool, ngx_log_t *log) { int rc, videoStream, frameFinished = 0, frameDecoded = 0; unsigned int i; AVFormatContext *pFormatCtx = NULL; AVCodecContext *pCodecCtx = NULL; AVCodec *pCodec = NULL; AVFrame *pFrame = NULL, *pFrameRGB = NULL; uint8_t *buffer = NULL; AVPacket packet; size_t uncompressed_size; float scale = 0.0, new_scale = 0.0, scale_sws = 0.0, scale_w = 0.0, scale_h = 0.0; int sws_width = 0, sws_height = 0; ngx_flag_t needs_crop = 0; MagickWand *m_wand = NULL; MagickBooleanType mrc; unsigned char *bufferAVIO = NULL; AVIOContext *pAVIOCtx = NULL; char *filename = (char *) info->filename->data; // Open video file if ((info->fd = fopen(filename, "rb")) == NULL) { ngx_log_error(NGX_LOG_ERR, log, 0, "video thumb extractor module: Couldn't open file %s", filename); rc = EXIT_FAILURE; goto exit; } // Get file size fseek(info->fd, 0, SEEK_END); info->size = ftell(info->fd) - info->offset; fseek(info->fd, 0, SEEK_SET); pFormatCtx = avformat_alloc_context(); bufferAVIO = (unsigned char *) av_malloc(NGX_HTTP_VIDEO_THUMBEXTRACTOR_BUFFER_SIZE * sizeof(unsigned char)); if ((pFormatCtx == NULL) || (bufferAVIO == NULL)) { ngx_log_error(NGX_LOG_ERR, log, 0, "video thumb extractor module: Couldn't alloc AVIO buffer"); rc = NGX_ERROR; goto exit; } pAVIOCtx = avio_alloc_context(bufferAVIO, NGX_HTTP_VIDEO_THUMBEXTRACTOR_BUFFER_SIZE, 0, info, ngx_http_video_thumbextractor_read_data_from_file, NULL, ngx_http_video_thumbextractor_seek_data_from_file); if (pAVIOCtx == NULL) { ngx_log_error(NGX_LOG_ERR, log, 0, "video thumb extractor module: Couldn't alloc AVIO context"); rc = NGX_ERROR; goto exit; } pFormatCtx->pb = pAVIOCtx; // Open video file if ((rc = avformat_open_input(&pFormatCtx, filename, NULL, NULL)) != 0) { ngx_log_error(NGX_LOG_ERR, log, 0, "video thumb extractor module: Couldn't open file %s, error: %d", filename, rc); rc = (rc == AVERROR(NGX_ENOENT)) ? NGX_HTTP_VIDEO_THUMBEXTRACTOR_FILE_NOT_FOUND : NGX_ERROR; goto exit; } // Retrieve stream information #if LIBAVFORMAT_VERSION_INT <= AV_VERSION_INT(53, 5, 0) if (av_find_stream_info(pFormatCtx) < 0) { #else if (avformat_find_stream_info(pFormatCtx, NULL) < 0) { #endif ngx_log_error(NGX_LOG_ERR, log, 0, "video thumb extractor module: Couldn't find stream information"); rc = NGX_ERROR; goto exit; } if ((pFormatCtx->duration > 0) && ((((float_t) pFormatCtx->duration / AV_TIME_BASE) - second)) < 0.1) { ngx_log_error(NGX_LOG_WARN, log, 0, "video thumb extractor module: seconds greater than duration"); rc = NGX_HTTP_VIDEO_THUMBEXTRACTOR_SECOND_NOT_FOUND; goto exit; } // Find the first video stream videoStream = -1; for (i = 0; i < pFormatCtx->nb_streams; i++) { if (pFormatCtx->streams[i]->codec->codec_type == AVMEDIA_TYPE_VIDEO) { videoStream = i; break; } } if (videoStream == -1) { ngx_log_error(NGX_LOG_ERR, log, 0, "video thumb extractor module: Didn't find a video stream"); rc = NGX_ERROR; goto exit; } // Get a pointer to the codec context for the video stream pCodecCtx = pFormatCtx->streams[videoStream]->codec; // Find the decoder for the video stream if ((pCodec = avcodec_find_decoder(pCodecCtx->codec_id)) == NULL) { ngx_log_error(NGX_LOG_ERR, log, 0, "video thumb extractor module: Codec %d not found", pCodecCtx->codec_id); rc = NGX_ERROR; goto exit; } // Open codec #if LIBAVCODEC_VERSION_INT <= AV_VERSION_INT(53, 8, 0) if ((rc = avcodec_open(pCodecCtx, pCodec)) < 0) { #else if ((rc = avcodec_open2(pCodecCtx, pCodec, NULL)) < 0) { #endif ngx_log_error(NGX_LOG_ERR, log, 0, "video thumb extractor module: Could not open codec, error %d", rc); rc = NGX_ERROR; goto exit; } if (height == 0) { // keep original format width = pCodecCtx->width; height = pCodecCtx->height; } else if (width == 0) { // calculate width related with original aspect width = height * pCodecCtx->width / pCodecCtx->height; } if ((width < 16) || (height < 16)) { ngx_log_error(NGX_LOG_ERR, log, 0, "video thumb extractor module: Very small size requested, %d x %d", width, height); rc = NGX_ERROR; goto exit; } scale = (float) pCodecCtx->width / pCodecCtx->height; new_scale = (float) width / height; sws_width = width; sws_height = height; if (scale != new_scale) { scale_w = (float) width / pCodecCtx->width; scale_h = (float) height / pCodecCtx->height; scale_sws = (scale_w > scale_h) ? scale_w : scale_h; sws_width = pCodecCtx->width * scale_sws + 0.5; sws_height = pCodecCtx->height * scale_sws + 0.5; needs_crop = 1; } // Allocate video frame pFrame = avcodec_alloc_frame(); // Allocate an AVFrame structure pFrameRGB = avcodec_alloc_frame(); if ((pFrame == NULL) || (pFrameRGB == NULL)) { ngx_log_error(NGX_LOG_ERR, log, 0, "video thumb extractor module: Could not alloc frame memory"); rc = NGX_ERROR; goto exit; } // Determine required buffer size and allocate buffer uncompressed_size = avpicture_get_size(PIX_FMT_RGB24, sws_width, sws_height) * sizeof(uint8_t); buffer = (uint8_t *) av_malloc(uncompressed_size); // Assign appropriate parts of buffer to image planes in pFrameRGB // Note that pFrameRGB is an AVFrame, but AVFrame is a superset of AVPicture avpicture_fill((AVPicture *) pFrameRGB, buffer, PIX_FMT_RGB24, sws_width, sws_height); if ((rc = av_seek_frame(pFormatCtx, -1, second * AV_TIME_BASE, cf->next_time ? 0 : AVSEEK_FLAG_BACKWARD)) < 0) { ngx_log_error(NGX_LOG_ERR, log, 0, "video thumb extractor module: Seek to an invalid time, error: %d", rc); rc = NGX_HTTP_VIDEO_THUMBEXTRACTOR_SECOND_NOT_FOUND; goto exit; } int64_t second_on_stream_time_base = second * pFormatCtx->streams[videoStream]->time_base.den / pFormatCtx->streams[videoStream]->time_base.num; // Find the nearest frame rc = NGX_HTTP_VIDEO_THUMBEXTRACTOR_SECOND_NOT_FOUND; while (!frameFinished && av_read_frame(pFormatCtx, &packet) >= 0) { // Is this a packet from the video stream? if (packet.stream_index == videoStream) { // Decode video frame avcodec_decode_video2(pCodecCtx, pFrame, &frameFinished, &packet); // Did we get a video frame? if (frameFinished) { frameDecoded = 1; if (!cf->only_keyframe && (pFrame->pkt_pts < second_on_stream_time_base)) { frameFinished = 0; } } } // Free the packet that was allocated by av_read_frame av_free_packet(&packet); } av_free_packet(&packet); if (frameDecoded) { // Convert the image from its native format to RGB struct SwsContext *img_resample_ctx = sws_getContext(pCodecCtx->width, pCodecCtx->height, pCodecCtx->pix_fmt, sws_width, sws_height, PIX_FMT_RGB24, SWS_BICUBIC, NULL, NULL, NULL); sws_scale(img_resample_ctx, (const uint8_t * const *) pFrame->data, pFrame->linesize, 0, pCodecCtx->height, pFrameRGB->data, pFrameRGB->linesize); sws_freeContext(img_resample_ctx); if (needs_crop) { MagickWandGenesis(); mrc = MagickTrue; if ((m_wand = NewMagickWand()) == NULL){ ngx_log_error(NGX_LOG_ERR, log, 0, "video thumb extractor module: Could not allocate MagickWand memory"); mrc = MagickFalse; } if (mrc == MagickTrue) { mrc = MagickConstituteImage(m_wand, sws_width, sws_height, NGX_HTTP_VIDEO_THUMBEXTRACTOR_RGB, CharPixel, pFrameRGB->data[0]); } if (mrc == MagickTrue) { mrc = MagickSetImageGravity(m_wand, CenterGravity); } if (mrc == MagickTrue) { mrc = MagickCropImage(m_wand, width, height, (sws_width-width)/2, (sws_height-height)/2); } if (mrc == MagickTrue) { mrc = MagickExportImagePixels(m_wand, 0, 0, width, height, NGX_HTTP_VIDEO_THUMBEXTRACTOR_RGB, CharPixel, pFrameRGB->data[0]); } /* Clean up */ if (m_wand) { m_wand = DestroyMagickWand(m_wand); } MagickWandTerminus(); if (mrc != MagickTrue) { ngx_log_error(NGX_LOG_ERR, log, 0, "video thumb extractor module: Error cropping image"); goto exit; } } // Compress to jpeg if (ngx_http_video_thumbextractor_jpeg_compress(cf, pFrameRGB->data[0], pCodecCtx->width, pCodecCtx->height, width, height, out_buffer, out_len, uncompressed_size, temp_pool) == 0) { rc = NGX_OK; } } exit: if ((info->fd != NULL) && (fclose(info->fd) != 0)) { ngx_log_error(NGX_LOG_ERR, log, 0, "video thumb extractor module: Couldn't close file %s", filename); rc = EXIT_FAILURE; } /* destroy unneeded objects */ // Free the RGB image if (buffer != NULL) av_free(buffer); if (pFrameRGB != NULL) av_freep(&pFrameRGB); // Free the YUV frame if (pFrame != NULL) av_freep(&pFrame); // Close the codec if (pCodecCtx != NULL) avcodec_close(pCodecCtx); // Close the video file if (pFormatCtx != NULL) { #if LIBAVFORMAT_VERSION_INT <= AV_VERSION_INT(53, 5, 0) av_close_input_file(pFormatCtx); #else avformat_close_input(&pFormatCtx); #endif } // Free AVIO context if (pAVIOCtx != NULL) av_freep(pAVIOCtx); return rc; } static void ngx_http_video_thumbextractor_init_libraries(void) { // Register all formats and codecs av_register_all(); av_log_set_level(AV_LOG_ERROR); } static uint32_t ngx_http_video_thumbextractor_jpeg_compress(ngx_http_video_thumbextractor_loc_conf_t *cf, uint8_t * buffer, int in_width, int in_height, int out_width, int out_height, caddr_t *out_buffer, size_t *out_len, size_t uncompressed_size, ngx_pool_t *temp_pool) { struct jpeg_compress_struct cinfo; struct jpeg_error_mgr jerr; JSAMPROW row_pointer[1]; int row_stride; int image_d_width = in_width; int image_d_height = in_height; if ( !buffer ) return 1; cinfo.err = jpeg_std_error(&jerr); jpeg_create_compress(&cinfo); ngx_http_video_thumbextractor_jpeg_memory_dest(&cinfo, out_buffer, out_len, uncompressed_size, temp_pool); cinfo.image_width = out_width; cinfo.image_height = out_height; cinfo.input_components = 3; cinfo.in_color_space = JCS_RGB; jpeg_set_defaults(&cinfo); /* Important: Header info must be set AFTER jpeg_set_defaults() */ cinfo.write_JFIF_header = TRUE; cinfo.JFIF_major_version = 1; cinfo.JFIF_minor_version = 2; cinfo.density_unit = 1; /* 0=unknown, 1=dpi, 2=dpcm */ /* Image DPI is determined by Y_density, so we leave that at jpeg_dpi if possible and crunch X_density instead (PAR > 1) */ if (out_height * image_d_width > out_width * image_d_height) { image_d_width = out_height * image_d_width / image_d_height; image_d_height = out_height; } else { image_d_height = out_width * image_d_height / image_d_width; image_d_width = out_width; } cinfo.X_density = cf->jpeg_dpi * out_width / image_d_width; cinfo.Y_density = cf->jpeg_dpi * out_height / image_d_height; cinfo.write_Adobe_marker = TRUE; jpeg_set_quality(&cinfo, cf->jpeg_quality, cf->jpeg_baseline); cinfo.optimize_coding = cf->jpeg_optimize; cinfo.smoothing_factor = cf->jpeg_smooth; if ( cf->jpeg_progressive_mode ) { jpeg_simple_progression(&cinfo); } jpeg_start_compress(&cinfo, TRUE); row_stride = out_width * 3; while (cinfo.next_scanline < cinfo.image_height) { row_pointer[0] = &buffer[cinfo.next_scanline * row_stride]; (void)jpeg_write_scanlines(&cinfo, row_pointer,1); } jpeg_finish_compress(&cinfo); jpeg_destroy_compress(&cinfo); return 0; } typedef struct { struct jpeg_destination_mgr pub; /* public fields */ unsigned char **buf; size_t *size; size_t uncompressed_size; ngx_pool_t *pool; } ngx_http_video_thumbextractor_jpeg_destination_mgr; static void ngx_http_video_thumbextractor_init_destination (j_compress_ptr cinfo) { ngx_http_video_thumbextractor_jpeg_destination_mgr * dest = (ngx_http_video_thumbextractor_jpeg_destination_mgr *) cinfo->dest; *(dest->buf) = ngx_palloc(dest->pool, dest->uncompressed_size); *(dest->size) = dest->uncompressed_size; dest->pub.next_output_byte = *(dest->buf); dest->pub.free_in_buffer = dest->uncompressed_size; }
//--------------------------------------------------------------------- // 出力プラグイン処理本体 //--------------------------------------------------------------------- BOOL func_output( OUTPUT_INFO *oip ) { const double mabiki = 1; //2にすると2フレーム中1フレームの間引き //intだと四捨五入の計算ができない if( oip->n > 500 / mabiki ) if( MessageBox( NULL, (LPCSTR) "大量のフレームが選択されています。\n本当に続行しますか?", (LPCSTR) "アニメーションGIF出力プラグイン", MB_YESNO | MB_ICONQUESTION ) == IDNO ) return TRUE; const int delay = round( mabiki * 100 * oip->scale / oip->rate ); //間引き×100÷フレームレートを四捨五入 MagickWandGenesis(); MagickWand *dest = NewMagickWand(); for( int i = 0; i < oip->n; i = i + mabiki ) { if( oip->func_is_abort() ) { if( MessageBox( NULL, (LPCSTR) "ここまでの出力データをアニメーションGIFに書き込みますか?", (LPCSTR) "アニメーションGIF出力プラグイン", MB_YESNO | MB_ICONQUESTION ) == IDYES ) break; else { DestroyMagickWand( dest ); MagickWandTerminus(); return TRUE; } } oip->func_rest_time_disp( i, oip->n ); int copy = 0; //コピーフレーム数 for( int j = 1; i + j < oip->n; j++ ) { if( oip->func_get_flag( i + j ) & OUTPUT_INFO_FRAME_FLAG_COPYFRAME ) copy++; else break; } MagickWand *source = NewMagickWand(); if( !MagickConstituteImage( source, oip->w, oip->h, "BGR", CharPixel, oip->func_get_video_ex( i, 0 ) ) ) //NULLだと警告 MessageBox( NULL, (LPCSTR) "データ取得失敗", (LPCSTR) "アニメーションGIF出力プラグイン", MB_OK|MB_ICONSTOP ); MagickFlipImage( source ); //AviUtlからはボトムアップで渡されるが、MagickConstituteImageはトップダウン固定 MagickSetImageDelay( source, delay * ( copy + 1 ) ); //コピーフレーム数0なら1倍 //MagickSetImageDispose( source, 0 ); MagickAddImage( dest, source ); DestroyMagickWand( source ); i = i + copy; //コピーフレーム数分だけ先送り oip->func_update_preview(); } MagickSetFirstIterator( dest ); MagickQuantizeImages( dest, 256, RGBColorspace, 0, FloydSteinbergDitherMethod, MagickFalse ); //MagickSetFirstIterator( dest ); //MagickSetImageIterations( dest, 0 ); if( !MagickSetFormat( dest, "GIF" ) ) MessageBox( NULL, (LPCSTR) "GIFセット失敗", (LPCSTR) "アニメーションGIF出力プラグイン", MB_OK|MB_ICONSTOP ); dest = MagickOptimizeImageLayers( MagickCoalesceImages( dest ) ); //ここでdisposeが1になる MagickOptimizeImageTransparency( dest ); //6.7.8-7以降が必要 if( !MagickWriteImages( dest, oip->savefile, MagickTrue ) ) MessageBox( NULL, (LPCSTR) "出力失敗", (LPCSTR) "アニメーションGIF出力プラグイン", MB_OK|MB_ICONSTOP ); DestroyMagickWand( dest ); MagickWandTerminus(); return TRUE; }