用 Delphi 7 实现基于 FFMS2 的视频转 GIF 工具 [原创]
儿子经常要把自拍的视频(ts格式)转成表情包,下载了几个工具都不大好用,更多的还要收费。那就想自己写一个吧,没想到这一下断断续续地,居然 3 个月过去了。现在总算弄出个作品来了,结个贴吧。唉,天资愚钝啊!
基本需求
- 能解码绝大部分视频格式;
- 能逐帧查看视频画面,并选择视频片段;
- 能框选画面中的某个区域;
- 对选中的视频片段、区域截图,生成 GIF 图片;
- GIF 图片能预览和保存为文件。
设计思路
界面
界面由 3 部分组成,自上而下分别是:视频播放区(TPanel)、控制区(TTrackBar + TSpeedButton) 、GIF 播放区(TImage + TMaskEdit)。
视频播放区
本工具并非视频播放软件,直接用 TImage 的 TCanvas 来显示视频帧画面,也没什么太大关系,卡不卡无关紧要。为了最大程度降低播放时的资源占用率,还是没有采用这种最简单的方案。也曾研究了一周 DirectX 或 SDL,性能是最佳,但没啥必要。最终选择了折衷的方案,直接用 Windows API DrawDibDraw 在 TPanel 上绘制。可参阅本人的另一篇文章对StretchBlt、StretchDIBits、DrawDibDraw 的性能测试。
但是在选取画面区域时,还是出现了问题。因为自己编写的 RectTracker,依赖于控件自身 Canvas 的重绘,所以会把视频播放 TPanel 上已绘制的画面给抹了。简化起见,放弃了对视频播放 TPanel 的重绘控制,而是在 TPanel 上叠加了一个 TImage,专门用于显示 RectTracker。反正在连续播放视频时,不需要显示画面选择框,逐帧步进时,多画一个 TImage 也无伤大雅。这也在播放性能和画面绘制取得了一点平衡。关于自制 RectTracker,可参阅本人的另一篇文章Delphi 中的 RectTracker。
控制区
视频播放的进度控制和按钮区,主要实现视频逐帧播放、截图、GIF 生成和保存等功能。
GIF 播放区
利用 GifImage 控件实现 GIF 生成、TImage 中播放、文件保存等功能。
主程序
unit main;
interface
uses
Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
Dialogs, ExtCtrls, StdCtrls, ComCtrls,Buttons, Mask, Menus,
ffmpeg, MMTimer, GifImage, DXRectTracker, DXTrackBar,
progress, about;
const
EXENAME = 'DJ''s V2G';
type
TMainForm = class(TForm)
dlgOpenFile: TOpenDialog;
panControl: TPanel;
btnLeft: TSpeedButton; // 后退一帧按钮
btnRight: TSpeedButton; // 前进一帧按钮
btnPlay: TSpeedButton; // 播放按钮
popMenuTrackbar: TPopupMenu; // 进度控制条的右键菜单
menuSetStart: TMenuItem; // 进度控制条的右键菜单项,标记 GIF 截频的起始帧
menuSetEnd: TMenuItem; // 进度控制条的右键菜单项,标记 GIF 截频的结束帧
btnOpenFile: TSpeedButton; // 打开文件按钮
btnSetStart: TSpeedButton; // 标记 GIF 截频的起始帧按钮
btnSetEnd: TSpeedButton; // 标记 GIF 截频的结束帧按钮
txtTime: TMaskEdit; // 显示当前播放时间(这里不妨可以实现按时间跳转)
btnPause: TSpeedButton; // 暂停播放按钮
btnToGif: TSpeedButton; // 将当前截取的画面转换为 GIF
panGif: TPanel;
Label1: TLabel;
Label2: TLabel;
dlgSaveFile: TSaveDialog;
Label3: TLabel;
txtGifFPS: TMaskEdit; // 显示/设置 GIF 的显示速度(每秒帧数)
txtGIFWidth: TMaskEdit; // 显示/设置 GIF 的宽度
txtGIFHeight: TMaskEdit; // 显示/设置 GIF 的高度
panPlayer: TPanel; // 视频播放区域
Trackbar: TDXTrackBar; // 视频播放进度控制条(自定义控件)
Panel1: TPanel;
imgGif: TImage; // GIF 显示控件
btnSave: TSpeedButton; // GIF 保存按钮
btnPlayAt: TSpeedButton; // 播放已截取片段视频
MainMenu: TPopupMenu;
MenuAbout: TMenuItem;
procedure btnOpenFileClick(Sender: TObject);
procedure btnPlayClick(Sender: TObject);
procedure FormCreate(Sender: TObject);
procedure FormClose(Sender: TObject; var Action: TCloseAction);
procedure TrackBarMouseUp(Sender: TObject; Button: TMouseButton;
Shift: TShiftState; X, Y: Integer); //
procedure TrackBarKeyUp(Sender: TObject; var Key: Word;
Shift: TShiftState); //
procedure btnLeftClick(Sender: TObject);
procedure btnRightClick(Sender: TObject);
procedure menuSetStartClick(Sender: TObject);
procedure menuSetEndClick(Sender: TObject);
procedure btnPauseClick(Sender: TObject);
procedure btnToGifClick(Sender: TObject);
procedure btnSaveClick(Sender: TObject);
procedure btnPlayGifClick(Sender: TObject);
procedure FormResize(Sender: TObject);
procedure txtGIFHeightChange(Sender: TObject);
procedure txtGIFWidthChange(Sender: TObject);
procedure btnPlayAtClick(Sender: TObject);
procedure FormContextPopup(Sender: TObject; MousePos: TPoint;
var Handled: Boolean);
private
{ Private declarations }
FCanSetPosition:boolean; // 标志可否设置进度条的位置,因为 Trackbar 的事件有限,只好用该标志同步,保证能同时实现进度控制和显示。
FSelectionMessage:string; // 当前选中时段的文字信息,暂时未用上
procedure CheckGifSize; // 根据视频区的选框自动设置 GIF 的大小
procedure CheckSize; // 根据视频的长宽比、窗口大小自动设置三大区域的大小
procedure ShowSelection; // 显示视频区中的选框
procedure OnFileLoaded(Sender:TObject); // TFFMpegPlayer 视频文件加载完毕的回调事件
procedure OnPlayerRefreshed(Sender:TObject); // TFFMpegPlayer 画面更新后的回调事件
procedure OnPlayerPaused(Sender:TObject); // TFFMpegPlayer 暂停播放后的回调事件
procedure OnPlayerError(Sender:TObject); // TFFMpegPlayer 出错后的回调事件
procedure OnPlayerStartIndex(Sender:TObject; ExpectedCount: integer); // TFFMpegPlayer 开始创建帧索引后的回调事件,暂时未用
procedure OnPlayerEndIndex(Sender:TObject); // TFFMpegPlayer 帧索引建立完成之后的回调事件,暂时未用
procedure OnPlayerIndexChanged(Sender:TObject; index: integer); // 暂时未用
procedure OnPlayerSelectionResize(Sender:TObject); // 视频区的选框调整回调事件
public
{ Public declarations }
end;
var
MainForm: TMainForm;
Player: TFFMpegPlayer; // 视频解码、播放、截图控件(自定义控件)
其中,FFMpegPlayer、DXRectTracker、DXTrackBar 控件均为自定义控件,分别实现了视频播放、屏幕选框、进度控制条功能。MMTimer 控件则为精确计时器,用于精确控制播放时间。GifImage 则对 TImage 进行了扩展,实现了 GIF 图片的显示、创建和保存功能。
视频播放
视频播放器是个很大的话题,有些还得支持网络流读取。实现音视频的同步也有很多种方法,比如 AegiSub 是以物理时间为基础的,全都以当前时间作为控制参照。有兴趣的,或者也可以去看看 FFMPEG 官方的 FFPlayer 实现。
本程序不需要处理音频信息,播放视频本也不是主要功能,所以能省则省,简而化之。思路大致如下,详见 TFFMpegPlayer 的源码:
- 播放采用单独的工作线程(TPlaybackThread);
- 工作线程唤醒后只向前读取并播放一帧,然后挂起;
- 精确计时器(FTimer)负责唤醒播放工作线程,时间间隔由视频文件 FPS 而定;
- 开始播放、停止播放的动作,只需要启用、禁用计时器即可;
- 用变量记录当前帧号、结束帧号,播放均以帧号为基础,不以时间戳为基础;
这里会有一个问题,读取并播放一帧的时间必须得小于 1/FPS 才行。如果视频分辨率很高(比如 4K)、CPU 不够快,视频看起来就是慢动作。这对于播放器程序而言是致命的,但对于本程序却无关紧要。当读取/解压速度固定时,只能提高播放速度了,换用 DirectX 或 OpenGL(SDL)吧。
画面框选
C 里有个 CRectTracker 控件,可以直接拿来用。可是 Delphi 里居然没有这个控件,只好自己写一个了。详见 Delphi 中的 RectTracker。因为要用 Canvas 绘制界面,所以继承的是 TGraphicControl。
进度控制条
Delphi 的 TTrackBar 是没法响应鼠标动作的,也就是说无法合理控制进度条。又只好自己动手了。原理可参阅带OnMouseUp、OnMouseDown、OnMouseMove事件的TrackBar控件。
因为是可视化控件,为了方便布局,还是得先安装一下才好用。不然打开项目会报错。
关于视频解码库
FFMPEG 陡峭的学习曲线
当然 FFMPG 是不二之选了,但实在是难啃啊。前后花了近 1 个月,还没摸到啥门道,一直是“按了葫芦起了瓢”的状态。其实也不能怪 FFMPEG,谁让视频格式那么庞杂无序,光一个 PTS 就没啥标准可统,基本就是靠猜!对于逐帧定位并播放的需求,用 FFMPEG 实现真是太过“谋乱”了。
虽然最终放弃了直接调用 FFMEPG 库,但这 1 个月也不算是白花,摸索 FFMPEG 的使用过程,也就是了解视频格式及播放原理的过程。由于 FFMPEG 没有什么好文档,各版本之间的差异也很大,一切尽在代码注释中。
建议不管出于什么目的,要了解 FFMPEG,请务必:
- 下个 FFMPEG源码;
- 装个 Visual Studio 2015。
然后,你就可以利用微软宇宙无敌的编辑器,方便地查看源码了,比千辛万苦在网上搜要强多了,至少我就是这么干的。
FFMPEG 的开发库还是在 zeranoe下载吧,想在 Windows 下自己编译还是有相当难度的,我是没这个时间去搞了。
FFMPEG 的学习资源
雷神的帖子必须读!虽然有几篇是翻老外的,但大都是通俗易懂啊。都不长,请全读一遍,下载源码编译运行一下就能明白。我一开始主要参考的是最简单的基于FFMPEG+SDL的视频播放器 ver2。可惜仙人已逝,看来天堂也缺视频处理的程序员啊!
此外,我还读了一些 FFMPEG 的文章,有用的大概有:
- FFMPEG 如何提取码流的基本信息
- How to Write a Video Player in Less Than 1000 Lines
- H264编码原理以及I帧B帧P帧
- ffmpeg time_base详解
- FFmpeg解码流程
强大的 FFMS2
因为在字幕组用的是 AegiSub,所以就很好奇,它到底用了什么招让视频能逐帧播放呢?虽然编译 dll 死活没成功,但用 Visual Studio 2015 查看源码还是挺方便的。原来 AegiSub 用的就是 FFMS2,于是就一头栽进 FFMS2 去泡着了。我对 FFMS2 的官方说明 做了翻译。
FFMS2 并没有将所有 FFMPEG 的源码都包含进来,只是选取了解码这部分,所以 dll 比完整的 FFMPEG 小多了,大约只有一半大小,这个适合我。
为了精确读取每一帧,FFMS2 必须先对视频、音频进行索引。如果视频文件很大,这个过程就很耗时,这里未及处理,程序会假死。倒是有个 API 可以设置显示进度的回调函数(FFMS_SetProgressCallback),可那是给 C 用的,不知 Delphi 能否成功,期待有高手解决。
在 Delphi 中调用 FFMS2.dll,得注意参数的声明,详见 LibFFMS2.pas。详情可参阅cdecl、stdcall、fastcall、declspec 的用法和区别。
结束语
虽然有了这么个半成品,但有待完善的地方太多了,比如对耗时较多的步骤没有进度显示(程序假死)、某些视频格式(关键帧较少)无法正确逐帧截图。权当抛砖引玉吧。
源码
可执行文件
FFMS2 库
FFMS2 源码 及 我的译文
FFMS2 API文档 及 我的译文
FFMPEG 源码 及 开发库
用 Delphi 7 实现基于 FFMS2 的视频转 GIF 工具 [原创]的更多相关文章
- Delphi 7中对StretchBlt, StretchDIBits, DrawDibDraw, BitBlt 的性能测试 - 原创
我的天哪,上一篇博文是2年前的事情了.看来又虚度了2年光阴,继续学习... 本文算是副产品,正品是利用FFmpeg从任意视频中生成GIF片段的小程序,等写完了再发.不为别的,只是为了给儿子做动图,且看 ...
- 多媒体(3):基于WindowsAPI的视频捕捉卡操作
目录 多媒体(1):MCI接口编程 多媒体(2):WAVE文件格式分析 多媒体(3):基于WindowsAPI的视频捕捉卡操作 多媒体(4):JPEG图像压缩编码 多媒体(3):基于WindowsAP ...
- 基于live555的视频直播 DM368IPNC RTSP分析
因需要,从个人的理解顺序和需求角度对live555的分析与开发整理,包含RTSP Server与RTSP Client.如何直播H.264流与JPEG流等,均进行了探讨,对live555的初学者有一定 ...
- VS2010 C++学习(5):基于DirectShow的视频预览录像程序
VS2010 C++学习(5):基于DirectShow的视频 预览录像程序 学习VC++编制的基于DirectShow视频捕获程序,主要练习基于DirectShow程序的应用. 一. ...
- 基于GIS的视频管理指挥平台
平台利用空间地理信息技术,以GIS地图为基础,将各类信息空间化.可视化,实现基于空间电子地图的可视化查询和分析,它能使情报.推理.分析与其他可用数据融为一体,提供依托于电子地图的清晰而精确的现场态势图 ...
- iOS:基于RTMP的视频推流
iOS基于RTMP的视频推流 一.基本介绍 iOS直播一出世,立马火热的不行,各种直播平台如雨后春笋,正因为如此,也同样带动了直播的技术快速发展,在IT界精通直播技术的猴子可是很值钱的.直播技术涉及的 ...
- OpenCV + python 实现人脸检测(基于照片和视频进行检测)
OpenCV + python 实现人脸检测(基于照片和视频进行检测) Haar-like 通俗的来讲,就是作为人脸特征即可. Haar特征值反映了图像的灰度变化情况.例如:脸部的一些特征能由矩形特征 ...
- 基于opencv将视频转化为字符串Java版
基于opencv将视频转化为字符串Java版 opencv java 先上一个效果图吧 首先,弄清一下原理 我们要将视频转化为字符画,那么就需要获取画面的每一帧,也就是每一张图片,然后将图片进行转化 ...
- 基于Video4Linux的视频采集模块开发(转)
Linux系统中,摄像头驱动程序安装好后,为了进行视频采集必须加入Video4Linux模块,从而可以通过Video4Linux模块提供的编程接口(API)从摄像头设备中获取图像帧.下面具体研究基于V ...
随机推荐
- 1055. [HAOI2008]玩具取名【区间DP】
Description 某人有一套玩具,并想法给玩具命名.首先他选择WING四个字母中的任意一个字母作为玩具的基本名字.然后 他会根据自己的喜好,将名字中任意一个字母用“WING”中任意两个字母代替, ...
- js中css样式兼容各个浏览器写法
在实际业务中往往需要在js中对dom添加一些样式,还需要对各个浏览器厂商的兼顾,看到一位大神写的一个方法很赞,做一个笔记 function prefixStyle(style){ var eleSty ...
- Python之Web2py框架使用
本文主要是对Web2py框架的介绍和安装使用. 一. 介绍 全栈式Web框架:Web2py是 Google 在 web.py 基础上二次开发而来的,兼容 Google App Engine .是一个为 ...
- Linux搜索查找命令
Linux搜索查找指令 find,用于在文件树中查找文件并作相应的处理 -name:按照文件名查找文件 -perm:按照文件权限查找文件 -user:按照文件属主来查找文件 -size:按照指定的文件 ...
- HDU1002 A + B Problem II 大数问题
题目链接: http://acm.hdu.edu.cn/showproblem.php?pid=1002 A + B Problem II Time Limit: 2000/1000 MS (Java ...
- 数据结构基础(1)--数组C语言实现--动态内存分配
数据结构基础(1)--数组C语言实现--动态内存分配 基本思想:数组是最常用的数据结构,在内存中连续存储,可以静态初始化(int a[2]={1,2}),可以动态初始化 malloc(). 难点就是数 ...
- warning:ISO C90 forbids mixed declarations and code
warning:ISO C90 forbids mixed declarations and code 变量定义之前不论什么一条非变量定义的语句(重视:语句是会带分号的)都会引起这个警告! 将非变量的 ...
- 1.1《想成为黑客,不知道这些命令行可不行》(Learn Enough Command Line to Be Dangerous)——运行终端
终端是个允许我们运行命令行的程序,运行命令前,先打开它.在MacOS系统上,可以使用macOS应用 Spotlight来打开终端窗口,Spotlight也有其他两种方式触发,一种是键入⌘␣(comma ...
- html5复习--canvas
一.简介 <canvas>是html5新增的标签,可以使用脚本(通常为JavaScript)在其中绘制图像的 HTML 元素.它可以用来制作照片集或者制作简单(也不是那么简单)的动画,甚至 ...
- oracle12c创建用户指定表空间
--1.创建临时空间 create temporary tablespace zyj_temp tempfile 'D:\app2\user\virtual\oradata\orcl\zyj_temp ...