視訊提取圖片/圖片合成視訊ffmpeg(二十)
前言
- 需求場景1(視訊中提取照片):
各大網站線上播放視訊時,滑鼠滑到某一時刻能夠提前顯示那一時刻的畫面。短的視訊編輯APP中,為了更好的對視訊進行編輯,會提取出視訊各個時刻的畫面進行預覽,那麼這些是如何實現的呢?本文將給出基於ffmpeg的實現程式碼以及實現思路。 - 需求場景2(照片合成視訊):
攝影師經常不間斷的拍攝一組連續的畫面用於合成延時視訊,剪印APP中也有時光相簿這樣通過照片生成視訊的功能(不過剪印APP照片合成的視訊採用了插值演算法生成了額外的過度動畫照片以及特效,功能更加複雜,但是不管怎樣,最終還是會由照片合成視訊)。本文基於ffmpeg實現簡單的照片合成視訊思路以及詳細程式碼
實現思路分析
這裡照片以JPG為例,視訊以MP4為例,其它格式類似
- 視訊中提取照片:
1、fmpeg對將畫素資料寫入到JPG圖片中也封裝到了avformat_xxx系列介面中,它的使用流程和封裝視訊資料到mp4檔案一模一樣,只不過一個JPG檔案中只包含了一幀視訊資料而已;
2、ffmpeg對JPG檔案的封裝支援模式匹配,即如果想要將多張圖片寫入到多張jpg中只需要檔名包含百分號即可,例如 name%3d.jpg,那麼在每一次呼叫av_write_frame()函式寫入視訊資料到jpg圖片時都會生成一張jpg圖片。這樣做的好處是不需要每一張要寫入的jpg檔案都建立一個AVFormatContext與之對應。其它流程和寫入一張jpg一樣。
流程為:
1、先從MP4中提取指定時刻AVPacket解碼成AVFrame
2、然後將步驟1得到的AVFrame進行從素格式YUV420P到JPG需要的YUVJ420P畫素格式的轉換
3、再重新編碼,然後再封裝到jpg中
- 照片合成視訊:
因為JPG的編碼方式為AV_CODEC_ID_MJPEG,MP4如果採用h264編碼,那麼兩者的編碼方式是不一致的,所以就需要先解碼再編碼,具體流程為:
1、先將JPG解碼成AVFrame
2、將JPG解碼後的源畫素格式YUVJ420P轉換成x264編碼需要的YUV420P畫素格式
3、再重新編碼,然後再封裝到mp4中
流程圖
-
視訊中提取照片流程圖
image.png
-
照片合成視訊:
image.png
實現程式碼
tips:要對jpg進行封裝和解封裝,編譯ffmpeg時要新增如下封裝器和解封裝
封裝器(及對應的編碼器)
--enable-muxer=image2
--enable-encoder=mjpeg
接封裝器(及對應的解碼器)
--enable-demuxer=image2
--enable-decoder=mjpeg
標頭檔案
#include <stdio.h>
#include <string>
extern "C" {
#include "cppcommon/CLog.h"
#include <libavutil/avutil.h>
#include <libavformat/avformat.h>
#include <libavcodec/avcodec.h>
#include <libswscale/swscale.h>
#include <libavutil/timestamp.h>
#include <libavutil/opt.h>
}
using namespace std;
class VideoJPG
{
public:
VideoJPG();
~VideoJPG();
/** 功能:實現提取任意時刻視訊的某一幀並將其轉化為JPG圖片輸出到當前目錄
*/
void doJpgGet();
/** 功能:將多張JPG照片合併成一段視訊
*/
void doJpgToVideo();
private:
AVFrame *de_frame;
AVFrame *en_frame;
// 用於視訊畫素轉換
SwsContext *sws_ctx;
// 用於讀取視訊
AVFormatContext *in_fmt;
// 用於解碼
AVCodecContext *de_ctx;
// 用於編碼
AVCodecContext *en_ctx;
// 用於封裝jpg
AVFormatContext *ou_fmt;
int video_ou_index;
void releaseSources();
void doDecode(AVPacket *in_pkt);
void doEncode(AVFrame *en_frame);
};
備註:
視訊提取圖片的實現位於doJpgGet();函式,圖片合成視訊位於doJpgToVideo();函式
實現檔案
#include "VideoJpg.hpp"
VideoJPG::VideoJPG()
{
sws_ctx = NULL;
de_frame = NULL;
en_frame = NULL;
in_fmt = NULL;
ou_fmt = NULL;
de_ctx = NULL;
en_ctx = NULL;
}
VideoJPG::~VideoJPG()
{
}
void VideoJPG::releaseSources()
{
if (in_fmt) {
avformat_close_input(&in_fmt);
in_fmt = NULL;
}
if (ou_fmt) {
avformat_free_context(ou_fmt);
ou_fmt = NULL;
}
if (en_frame) {
av_frame_unref(en_frame);
en_frame = NULL;
}
if (de_frame) {
av_frame_unref(de_frame);
de_frame = NULL;
}
if (en_ctx) {
avcodec_free_context(&en_ctx);
en_ctx = NULL;
}
if (de_ctx) {
avcodec_free_context(&de_ctx);
de_ctx = NULL;
}
}
/** 寫入jpg說明:
* 1、ffmpeg對將畫素資料寫入到JPG圖片中也封裝到了avformat_xxx系列介面中,它的使用流程和封裝視訊資料到mp4檔案一模一樣
* 只不過一個JPG檔案中只包含了一幀視訊資料而已;
* 2、ffmpeg對JPG檔案的封裝支援模式匹配,即如果想要將多張圖片寫入到多張jpg中只需要檔名包含百分號即可,例如 name%3d.jpg,那麼在每一次呼叫av_write_frame()
* 函式寫入視訊資料到jpg圖片時都會生成一張jpg圖片。這樣做的好處是不需要每一張要寫入的jpg檔案都建立一個AVFormatContext與之對應。其它流程和寫入一張jpg一樣,具體
* 參考如下示例:
* 3、jpg對應的封裝器為ff_image2_muxer,對應的編碼器為ff_mjpeg_encoder
*/
#define Get_More 1 // 1代表使用模式匹配,一次可以寫入多張jpg圖片。0代表一次寫入1張圖片
void VideoJPG::doJpgGet()
{
string curFile(__FILE__);
unsigned long pos = curFile.find("2-video_audio_advanced");
if (pos == string::npos) {
LOGD("not find file");
return;
}
string srcDic = curFile.substr(0,pos) + "filesources/";
string srcPath = srcDic + "test_1280x720_3.mp4";
#if Get_More
string dstPath = srcDic + "1-doJpg_get%3d.jpg";
int num = 5;
#else
string dstPath = srcDic + "1-doJpg_get.jpg";
#endif
int video_index = -1;
// 要擷取的時刻
string start = "00:00:05";
int64_t start_pts = stoi(start.substr(0,2));
start_pts += stoi(start.substr(3,2));
start_pts += stoi(start.substr(6,2));
if (avformat_open_input(&in_fmt,srcPath.c_str(),NULL,NULL) < 0) {
LOGD("avformat_open_input fail");
return;
}
if (avformat_find_stream_info(in_fmt, NULL) < 0) {
LOGD("avformat_find_stream_info fail");
return;
}
// 遍歷出視訊索引
for (int i = 0; i<in_fmt->nb_streams; i++) {
AVStream *stream = in_fmt->streams[i];
if (stream->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) { // 說明是視訊
video_index = i;
// 初始化解碼器用於解碼
AVCodec *codec = avcodec_find_decoder(stream->codecpar->codec_id);
de_ctx = avcodec_alloc_context3(codec);
if (!de_ctx) {
LOGD("video avcodec_alloc_context3 fail");
releaseSources();
return;
}
// 設定解碼引數,這裡直接從源視訊流中拷貝
if (avcodec_parameters_to_context(de_ctx, stream->codecpar) < 0) {
LOGD("video avcodec_parameters_to_context");
releaseSources();
return;
}
// 初始化解碼器上下文
if (avcodec_open2(de_ctx, codec, NULL) < 0) {
LOGD("video avcodec_open2() fail");
releaseSources();
return;
}
break;
}
}
// 初始化編碼器;因為最終是要寫入到JPEG,所以使用的編碼器ID為AV_CODEC_ID_MJPEG
AVCodec *codec = avcodec_find_encoder(AV_CODEC_ID_MJPEG);
en_ctx = avcodec_alloc_context3(codec);
if (!en_ctx) {
LOGD("avcodec_alloc_context3 fail");
releaseSources();
return;
}
// 設定編碼引數
AVStream *in_stream = in_fmt->streams[video_index];
en_ctx->width = in_stream->codecpar->width;
en_ctx->height = in_stream->codecpar->height;
// 如果是編碼後寫入到圖片中,那麼位元率可以不用設定,不影響最終的結果(也不會影響影像清晰度)
en_ctx->bit_rate = in_stream->codecpar->bit_rate;
// 如果是編碼後寫入到圖片中,那麼幀率可以不用設定,不影響最終的結果
en_ctx->framerate = in_stream->r_frame_rate;
en_ctx->time_base = in_stream->time_base;
// 對於MJPEG編碼器來說,它支援的是YUVJ420P/YUVJ422P/YUVJ444P格式的畫素
en_ctx->pix_fmt = AV_PIX_FMT_YUVJ420P;
// 初始化編碼器上下文
if (avcodec_open2(en_ctx, codec, NULL) < 0) {
LOGD("avcodec_open2() fail");
releaseSources();
return;
}
// 建立用於輸出JPG的封裝器
if (avformat_alloc_output_context2(&ou_fmt, NULL, NULL, dstPath.c_str()) < 0) {
LOGD("avformat_alloc_output_context2");
releaseSources();
return;
}
/** 新增流
* 對於圖片封裝器來說,也可以把它想象成只有一幀視訊的視訊封裝器。所以它實際上也需要一路視訊流,而事實上圖片的流是視訊流型別
*/
AVStream *stream = avformat_new_stream(ou_fmt, NULL);
// 設定流引數;直接從編碼器拷貝引數即可
if (avcodec_parameters_from_context(stream->codecpar, en_ctx) < 0) {
LOGD("avcodec_parameters_from_context");
releaseSources();
return;
}
/** 初始化上下文
* 對於寫入JPG來說,它是不需要建立輸出上下文IO緩衝區的的,所以avio_open2()沒有呼叫到,但是最終一樣可以呼叫av_write_frame()寫入資料
*/
if (!(ou_fmt->oformat->flags & AVFMT_NOFILE)) {
if (avio_open2(&ou_fmt->pb, dstPath.c_str(), AVIO_FLAG_WRITE, NULL, NULL) < 0) {
LOGD("avio_open2 fail");
releaseSources();
return;
}
}
/** 為輸出檔案寫入頭資訊
* 不管是封裝音視訊檔案還是圖片檔案,都需要呼叫此方法進行相關的初始化,否則av_write_frame()函式會崩潰
*/
if (avformat_write_header(ou_fmt, NULL) < 0) {
LOGD("avformat_write_header() fail");
releaseSources();
return;
}
/** 建立視訊畫素轉換上下文
* 因為源視訊的畫素格式是yuv420p的,而jpg編碼需要的畫素格式是yuvj420p的,所以需要先進行畫素格式轉換
*/
sws_ctx = sws_getContext(in_stream->codecpar->width, in_stream->codecpar->height, (enum AVPixelFormat)in_stream->codecpar->format,
en_ctx->width, en_ctx->height, en_ctx->pix_fmt,
0, NULL, NULL, NULL);
if (!sws_ctx) {
LOGD("sws_getContext fail");
releaseSources();
return;
}
// 建立編碼解碼用的AVFrame
de_frame = av_frame_alloc();
en_frame = av_frame_alloc();
en_frame->width = en_ctx->width;
en_frame->height = en_ctx->height;
en_frame->format = en_ctx->pix_fmt;
if (av_frame_get_buffer(en_frame, 0) < 0) {
LOGD("av_frame_get_buffer fail");
releaseSources();
return;
}
if (av_frame_make_writable(en_frame) < 0) {
LOGD("av_frame_make_writeable fail");
releaseSources();
return;
}
AVPacket *in_pkt = av_packet_alloc();
AVPacket *ou_pkt = av_packet_alloc();
AVRational time_base = in_fmt->streams[video_index]->time_base;
AVRational frame_rate = in_fmt->streams[video_index]->r_frame_rate;
// 一幀的時間戳
int64_t delt = time_base.den/frame_rate.num;
start_pts *= time_base.den;
/** 因為想要擷取的時間處的AVPacket並不一定是I幀,所以想要正確的解碼,得先找到離想要擷取的時間處往前的最近的I幀
* 開始解碼,直到拿到了想要獲取的時間處的AVFrame
* AVSEEK_FLAG_BACKWARD 代表如果start_pts指定的時間戳處的AVPacket非I幀,那麼就往前移動指標,直到找到I幀,那麼
* 當首次呼叫av_frame_read()函式時返回的AVPacket將為此I幀的AVPacket
*/
if (av_seek_frame(in_fmt, video_index, start_pts, AVSEEK_FLAG_BACKWARD) < 0) {
LOGD("av_seek_frame fail");
releaseSources();
return;
}
bool found = false;
while (av_read_frame(in_fmt, in_pkt) == 0) {
if (in_pkt->stream_index != video_index) {
continue;
}
// 先解碼
avcodec_send_packet(de_ctx, in_pkt);
LOGD("video pts %d(%s)",in_pkt->pts,av_ts2timestr(in_pkt->pts,&in_stream->time_base));
while (true) {
int ret = avcodec_receive_frame(de_ctx, de_frame);
if (ret < 0) {
break;
}
/** 解碼得到的AVFrame中的pts和解碼前的AVPacket中的pts是一一對應的,所以可以利用AVFrame中的pts來判斷此幀是否在想要擷取的時間範圍內
*/
LOGD("sucess pts %d",de_frame->pts);
// 成功解碼出來了
#if Get_More
// 取多幀視訊並寫入到檔案
static int i=0;
delt = delt*num;
if (abs(de_frame->pts - start_pts) < delt) {
i++;
#else
// 去一幀幀視訊並寫入到檔案
if (abs(de_frame->pts - start_pts) < delt) {
#endif
LOGD("找到了這一幀");
// 因為源視訊幀的格式和目標視訊幀的格式可能不一致,所以這裡需要轉碼
ret = sws_scale(sws_ctx, de_frame->data, de_frame->linesize, 0, de_frame->height, en_frame->data, en_frame->linesize);
if (ret < 0) {
LOGD("sws_scale fail");
releaseSources();
return;
}
#if Get_More
// 重新編碼
en_frame->pts = i;
avcodec_send_frame(en_ctx, en_frame);
// 拿到指定數目的AVPacket後再清空緩衝區
if (i > num) {
avcodec_send_frame(en_ctx, NULL);
}
#else
// 重新編碼;因為只有一幀,所以這裡直接寫1 即可
en_frame->pts = 1;
avcodec_send_frame(en_ctx, en_frame);
// 因為只編碼一幀,所以傳送一幀視訊後立馬清空緩衝區
avcodec_send_frame(en_ctx, NULL);
#endif
ret = avcodec_receive_packet(en_ctx, ou_pkt);
if (ret < 0) {
LOGD("fail ");
releaseSources();
return;
}
// 寫入檔案
if(av_write_frame(ou_fmt, ou_pkt) < 0) {
LOGD("av_write_frame fail");
releaseSources();
return;
}
#if Get_More
if (i>num) {
found = true;
}
#else
found = true;
#endif
break;
}
}
av_packet_unref(in_pkt);
if (found) {
LOGD("has get jpg");
break;
}
}
/** 寫入檔案尾
* 對於寫入視訊檔案來說,此函式必須呼叫,但是對於寫入JPG檔案來說,不呼叫此函式也沒關係;
*/
// av_write_trailer(ou_fmt);
// 釋放資源
releaseSources();
}
/** 多張圖片合成為一段視訊說明:
* 1、ffmpeg對jpg的解封裝和對視訊的解封裝一樣,都封裝到了avformat_xxxx系列介面裡面。流程和解封裝音視訊的流程一模一樣,對一張jpg圖片的解封裝可以理解為對只包含一幀
* 視訊的視訊檔案的解封裝
* 2、ffmpeg對jpeg的解封裝支援模式匹配,例如對於name%3d.jpg進行解封裝時(假如目錄中包含的jpg列表為
* name001.jpg,name002.jpg,name004.jpg,........),每次呼叫av_frame_read()函式,它將按照name001.jpg,name002.jpg,name004.jpg,........的順序依次進行讀取
* 3、jpg對應的解封裝器為ff_image2_demuxer,對應的編碼器為ff_mjpeg_decoder
*/
/** 將前面視訊生成的jpg合成mp4檔案
* 因為JPG的編碼方式為AV_CODEC_ID_MJPEG,MP4如果採用h264編碼,那麼兩者的編碼方式是不一致的,所以就需要先解碼再編碼,具體流程為:
* 1、先將JPG解碼成AVFrame
* 2、將JPG解碼後的源畫素格式YUVJ420P轉換成x264編碼需要的YUV420P畫素格式
* 3、再重新編碼,然後再封裝到mp4中
*/
void VideoJPG::doJpgToVideo()
{
string curFile(__FILE__);
unsigned long pos = curFile.find("2-video_audio_advanced");
if (pos == string::npos) {
LOGD("not find file");
return;
}
string srcDic = curFile.substr(0,pos) + "filesources/";
string srcPath = srcDic + "1-doJpg_get%3d.jpg";
string dstPath = srcDic + "1-dojpgToVideo.mp4";
int video_index = -1;
// 建立jpg的解封裝上下文
if (avformat_open_input(&in_fmt, srcPath.c_str(), NULL, NULL) < 0) {
LOGD("avformat_open_input fail");
return;
}
if (avformat_find_stream_info(in_fmt, NULL) < 0) {
LOGD("avformat_find_stream_info()");
releaseSources();
return;
}
// 建立解碼器及初始化解碼器上下文用於對jpg進行解碼
for (int i=0; i<in_fmt->nb_streams; i++) {
AVStream *stream = in_fmt->streams[i];
/** 對於jpg圖片來說,它裡面就是一路視訊流,所以媒體型別就是AVMEDIA_TYPE_VIDEO
*/
if (stream->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) {
AVCodec *codec = avcodec_find_decoder(stream->codecpar->codec_id);
if (!codec) {
LOGD("not find jpg codec");
releaseSources();
return;
}
de_ctx = avcodec_alloc_context3(codec);
if (!de_ctx) {
LOGD("jpg codec_ctx fail");
releaseSources();
return;
}
// 設定解碼引數;檔案解封裝的AVStream中就包括瞭解碼引數,這裡直接流中拷貝即可
if (avcodec_parameters_to_context(de_ctx, stream->codecpar) < 0) {
LOGD("set jpg de_ctx parameters fail");
releaseSources();
return;
}
// 初始化解碼器及解碼器上下文
if (avcodec_open2(de_ctx, codec, NULL) < 0) {
LOGD("avcodec_open2() fail");
releaseSources();
return;
}
video_index = i;
break;
}
}
// 建立mp4檔案封裝器
if (avformat_alloc_output_context2(&ou_fmt,NULL,NULL,dstPath.c_str()) < 0) {
LOGD("MP2 muxer fail");
releaseSources();
return;
}
// 新增視訊流
AVStream *stream = avformat_new_stream(ou_fmt, NULL);
video_ou_index = stream->index;
// 建立h264的編碼器及編碼器上下文
AVCodec *en_codec = avcodec_find_encoder(AV_CODEC_ID_H264);
if (!en_codec) {
LOGD("encodec fail");
releaseSources();
return;
}
en_ctx = avcodec_alloc_context3(en_codec);
if (!en_ctx) {
LOGD("en_codec ctx fail");
releaseSources();
return;
}
// 設定編碼引數
AVStream *in_stream = in_fmt->streams[video_index];
en_ctx->width = in_stream->codecpar->width;
en_ctx->height = in_stream->codecpar->height;
en_ctx->pix_fmt = (enum AVPixelFormat)in_stream->codecpar->format;
en_ctx->bit_rate = 0.96*1000000;
en_ctx->framerate = (AVRational){5,1};
en_ctx->time_base = (AVRational){1,5};
// 某些封裝格式必須要設定,否則會造成封裝後檔案中資訊的缺失
if (ou_fmt->oformat->flags & AVFMT_GLOBALHEADER) {
en_ctx->flags |= AV_CODEC_FLAG_GLOBAL_HEADER;
}
// x264編碼特有
if (en_codec->id == AV_CODEC_ID_H264) {
// 代表了編碼的速度級別
av_opt_set(en_ctx->priv_data,"preset","slow",0);
en_ctx->flags2 = AV_CODEC_FLAG2_LOCAL_HEADER;
}
// 初始化編碼器及編碼器上下文
if (avcodec_open2(en_ctx,en_codec,NULL) < 0) {
LOGD("encodec ctx fail");
releaseSources();
return;
}
// 設定視訊流引數;對於封裝來說,直接從編碼器上下文拷貝即可
if (avcodec_parameters_from_context(stream->codecpar, en_ctx) < 0) {
LOGD("copy en_code parameters fail");
releaseSources();
return;
}
// 初始化封裝器輸出緩衝區
if (!(ou_fmt->oformat->flags & AVFMT_NOFILE)) {
if (avio_open2(&ou_fmt->pb, dstPath.c_str(), AVIO_FLAG_WRITE, NULL, NULL) < 0) {
LOGD("avio_open2 fail");
releaseSources();
return;
}
}
// 建立畫素格式轉換器
sws_ctx = sws_getContext(de_ctx->width, de_ctx->height, de_ctx->pix_fmt,
en_ctx->width, en_ctx->height, en_ctx->pix_fmt,
0, NULL, NULL, NULL);
if (!sws_ctx) {
LOGD("sws_getContext fail");
releaseSources();
return;
}
// 寫入封裝器標頭檔案資訊;此函式內部會對封裝器引數做進一步初始化
if (avformat_write_header(ou_fmt, NULL) < 0) {
LOGD("avformat_write_header fail");
releaseSources();
return;
}
// 建立編解碼用的AVFrame
de_frame = av_frame_alloc();
en_frame = av_frame_alloc();
en_frame->width = en_ctx->width;
en_frame->height = en_ctx->height;
en_frame->format = en_ctx->pix_fmt;
av_frame_get_buffer(en_frame, 0);
av_frame_make_writable(en_frame);
AVPacket *in_pkt = av_packet_alloc();
while (av_read_frame(in_fmt, in_pkt) == 0) {
if (in_pkt->stream_index != video_index) {
continue;
}
// 先解碼
doDecode(in_pkt);
av_packet_unref(in_pkt);
}
// 重新整理解碼緩衝區
doDecode(NULL);
av_write_trailer(ou_fmt);
LOGD("結束。。。");
// 釋放資源
releaseSources();
}
void VideoJPG::doDecode(AVPacket *in_pkt)
{
static int num_pts = 0;
// 先解碼
avcodec_send_packet(de_ctx, in_pkt);
while (true) {
int ret = avcodec_receive_frame(de_ctx, de_frame);
if (ret == AVERROR_EOF) {
doEncode(NULL);
break;
} else if(ret < 0) {
break;
}
// 成功解碼了;先進行格式轉換然後再編碼
if(sws_scale(sws_ctx, de_frame->data, de_frame->linesize, 0, de_frame->height, en_frame->data, en_frame->linesize) < 0) {
LOGD("sws_scale fail");
releaseSources();
return;
}
// 編碼前要設定好pts的值,如果en_ctx->time_base為{1,fps},那麼這裡pts的值即為幀的個數值
en_frame->pts = num_pts++;
doEncode(en_frame);
}
}
void VideoJPG::doEncode(AVFrame *en_frame1)
{
avcodec_send_frame(en_ctx, en_frame1);
while (true) {
AVPacket *ou_pkt = av_packet_alloc();
if (avcodec_receive_packet(en_ctx, ou_pkt) < 0) {
av_packet_unref(ou_pkt);
break;
}
// 成功編碼了;寫入之前要進行時間基的轉換
AVStream *stream = ou_fmt->streams[video_ou_index];
av_packet_rescale_ts(ou_pkt, en_ctx->time_base, stream->time_base);
LOGD("video pts %d(%s)",ou_pkt->pts,av_ts2timestr(ou_pkt->pts, &stream->time_base));
av_write_frame(ou_fmt, ou_pkt);
}
}
專案地址
https://github.com/nldzsz/ffmpeg-demo
位於cppsrc目錄下
VideoJpg.hpp/VideoJpg.cpp檔案
專案下示例可執行於iOS/android/mac平臺,工程分別位於demo-ios/demo-android/demo-mac三個目錄下,可根據需要選擇不同平臺
相關文章
- FFmpeg 圖片合成影片
- FFmpeg程式碼實現視訊轉jpg圖片
- 在python中將多張圖片合成為視訊Python
- iOS 獲取視訊圖片iOS
- 獲取本地圖片/視訊地圖
- 圖片轉化為視訊
- Python:圖片合視訊(最簡)Python
- 短視訊原始碼,視訊轉為圖片儲存原始碼
- Darkroom for Mac(圖片視訊編輯器)OOMMac
- 功能性模組: (5)圖片生成視訊:ffmpeg版和OpenCV版OpenCV
- 圖片、視訊損壞了,如何修復?
- UEditor 自定義圖片視訊尺寸校驗
- ffmpeg實戰-音視訊合成案例
- 短視訊app開發,短視訊動態功能上傳圖片時,規定圖片壓縮的大小APP
- 三種方法使用FFMPEG擷取視訊片斷
- 視訊音樂圖片格式轉換Permute 3
- [速記] Mac 下視訊 / 圖片批量轉碼Mac
- 短視訊原始碼,python使用post提交圖片原始碼Python
- 使用MediaCodeC將圖片集編碼為視訊
- 音視訊開發指南:圖片的繪製
- 短視訊直播原始碼,自定義圖片或視訊的迴圈播放原始碼
- 短視訊直播原始碼,動態釋出時選擇圖片、上傳圖片原始碼
- 短視訊平臺開發,圖片上傳和圖片預覽功能實現
- 短視訊平臺開發,將圖片、視訊儲存到本地的相簿中
- 短視訊app原始碼,點選檢視圖片,雙指放大APP原始碼
- 小視訊原始碼,java使用Thumbnails壓縮圖片原始碼JavaAI
- IM 聊天教程:傳送圖片 / 視訊 / 語音 / 表情
- golang 合成的圖片Golang
- PHP 圖片、文字合成PHP
- android短視訊開發,點選靜態圖片自動跳轉播放視訊Android
- Flutter上線專案實戰——圖片視訊預覽Flutter
- php Nginx修改上傳視訊或者大圖片的配置PHPNginx
- 視訊新增背景圖片初學者要怎麼操作?
- 短視訊平臺搭建,生成圖片形狀的位置
- 2019年最新微商圖片視訊處理工具大全
- 【秒懂音視訊開發】17_重識圖片
- 圖片視訊瀑布流長列表效能優化實踐優化
- win10系統jpg圖片詳細資訊怎麼檢視_win10系統jpg圖片詳細資訊如何檢視Win10