一、前言
大家好,俗话说的好,学习新的知识后要学以致用,在学习音视频的过程中,你有没有疑问,不知道音视频可以用来做什么。下面举几个例子,比较耳熟能详,被吹到风口的一些场景有:AI 视觉计算, AI 人脸识别. 细化到一些小的领域,如现在直播技术,摄像头监控拉流;其他还有抖音中的美颜,滤镜,其背后是使用的音视频领域的数字化妆技术。由此可见,音视频技术应用已经应用于我们生活的方方面面。
二、开发背景
想写这篇文章的目的是因为,我有个朋友平时喜欢刷抖音,就经常有一些视频被作者设置成了不可下载保存,朋友下次想再看的话就找不到了。 还有朋友想下载暗恋的妹纸的作品。所以就把苦闷告诉我了,作为朋友当然有义务帮助他走出困境啦,终于,Two thousand years later 的今天,这个小工具终于问世,因为时间原因,来不及写前端页面了,后面有需要的同学可以关注或者私信我,我们一起学习,另外,写本文的目的纯粹是以学习为主,如不小心被不法分子滥用以盈利为目的,与本人无关,请广大道友积极爱护学习环境,记得不要连累我。
三、核心思想
其实核心步骤就两步
1、根据抖音上复制的分享链接获取到抖音的真实地址,需要使用网络编程技术解析到视频的真实地址。
2、然后使用 ffmpeg 解码网络视频流,保存到本地。
四、主要技术点
1、主要使用 Java 与 一些网络调用的知识,例如 Restemplate 的使用,Restemplate 是 spring web 中的一个模板方法类,这里主要用到了他的两个方法(headForHeaders, exchange),当然也可以用其他的工具类或者自己去实现网络远程调用。
2、JSON 解析使用 fastjson,版本号随意,一般都可以兼容。
3、StringUtils 是使用 commons.lang3 包下面中的工具类,不要导错包啦。
4、ffmpeg 拉流使用的第三方依赖是 javacv,版本 1.4.3 版本。如需具体引用依赖,关注或者私信我。
5、如果你对于 ffmpeg 基本概念,音视频基本概念,如视频帧, 音频帧,码率等基本知识不是非常清楚,这里我只说技术的应用,关于原理的讲解,不做过多赘述,网上一搜一大堆,有兴趣可以自己去了解以下。
6、使用 javacv 中的 FFmpegFrameGrabber 帧抓取器来获取音/视频帧,用这个抓取器,可以省略原生的 API 调用的一堆复杂操作,例如打开视频流,查找解码器,判断音频帧和视频帧。
来自网上的一段介绍/概括
FFmpegFrameGrabber 用于采集/抓取视频图像和音频采样。封装了检索流信息,自动猜测视频解码格式,音视频解码等具体 API,并把解码完的像素数据(可配置像素格式)或音频数据保存到 Frame 中返回等功能。
7、还可以使用 ffmpeg 命令行的方式进行下载。命令如下:
ffmpeg -i https://xxx.mp4 -c copy -f flv 艾北.flv
复制代码
但是这种方式需要部署机安装 ffmpeg,所以暂时不考虑这种方式了。
8、使用 javacv 中的 FrameRecorder 录制器来把已经解码的图像像素编码成对应的编码和格式推流出去,这里保存到本地就是推流到本地文件。
以下是音视频大佬 eguid 对于 FrameRecorder 的介绍概括
FrameRecorder 用于音视频/图片的封装、编码、推流和录制保存等操作。把从 FrameGrabber 或者 FrameFilter 获取的 Frame 中的数据取出并进行编码、封装、推流发送等操作流程。为了方便理解和阅读,下文开始我们统一把 FrameRecorder 简称为:录制器。
五、详细思路
1、链接解析 &接口解析
(1)、Java 正则表达式从字符串中提取出 url。
(2)、使用 RestTemplate.headForHeaders() 方法获取某个资源的 URI 的请求头信息,并且只专注于获取 HTTP 请求头信息。
(3)、第一步中提取出的 url 在浏览器中模拟可以发现, 会重定向到一个新的地址,从请求头中获取重定向后的地址, 即从 header 中获取 location,然后从 location 中获取视频的真实 id。
(4)、根据视频真实 id 和抖音的接口去获取视频信息,如播放信息,作者信息,背景音乐信息等等,使用 json 一层一层解析出来播放地址的 url。
2、ffmpeg 拉流并保存
(1)、使用 ffmpeg 获取 url 视频帧的第一帧,检测视频是否是空视频。
(2)、创建视频流录制器,设置视频参数,分辨率,格式,输出位置。
(3)、循环获取视频帧,使用录制器 recorder 逐帧录制视频帧。
六、核心代码
1、使用正则提取 url
/**
* 正则表达式提取 url
* @param text
* @return
*/
public static String pickURI(String text) {
// eg: text = "5.1 GV:/ 一出场就给人一种江南的感觉%刘亦菲 %精彩片段 %歌曲红马 https://v.douyin.com/e614JkV/ 腹制佌lian接,打开Dou音搜索,直接观kan视頻!";
Pattern pattern = Pattern.compile("https?://[-A-Za-z0-9+&@#/%?=~_|!:,.;]+[-A-Za-z0-9+&@#/%=~_|]");
Matcher matcher = pattern.matcher(text);
if (matcher.find()) {
return matcher.group();
}
return "";
}
复制代码
2、发起网络调用,解析 json 获取真实地址
public final static String DOU_YIN_WEB_API = "https://www.iesdouyin.com/web/api/v2/aweme/iteminfo/?item_ids=";
/**
* 根据赋值的分享码下载抖音视频
* @param text
* @throws FrameGrabber.Exception
* @throws FrameRecorder.Exception
*/
public static String douYin(String text) throws FrameGrabber.Exception, FrameRecorder.Exception {
//
String url = pickURI(text);
RestTemplate client = new RestTemplate();
//
HttpHeaders headers = client.headForHeaders(url);
String location = headers.getLocation().toString();
String vid = StringUtils.substringBetween(location, "/video/", "/?");
RestTemplate restTemplate = new RestTemplate();
HttpHeaders queryHeaders = new HttpHeaders();
queryHeaders.set(HttpHeaders.USER_AGENT, "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.114 Safari/537.36");
HttpEntity<String> entity = new HttpEntity<>(queryHeaders);
ResponseEntity<JSONObject> response = restTemplate.exchange(DOU_YIN_WEB_API + vid, HttpMethod.GET, entity, JSONObject.class);
if(response.getStatusCodeValue() != 200) {
return "";
}
JSONObject body = response.getBody();
assert body != null;
List<JSONObject> list = JSONArray.parseObject(body.getJSONArray("item_list").toJSONString(), new TypeReference<List<JSONObject>>(){});
if(list.size() == 0) {
return "";
}
JSONObject item = list.get(0);
JSONObject video = item.getJSONObject("video");
JSONObject playAddr = video.getJSONObject("play_addr");
JSONArray urlList = playAddr.getJSONArray("url_list");
List<String> urlListArr = JSONArray.parseObject(urlList.toJSONString(), new TypeReference<List<String>>(){});
if(urlListArr.size() == 0) {
return "";
}
return urlListArr.get(0);
// VideoConvert.record(finalAddr, "/home/yinyue/upload/红马.flv");
}
复制代码
3、ffmpeg 拉流并保存
/**
* 转存视频流
* @param input
* @param outFile
* @throws FrameGrabber.Exception
* @throws FrameRecorder.Exception
*/
public static void record(String input, String outFile) throws FrameGrabber.Exception, FrameRecorder.Exception {
FrameGrabber grabber = new FFmpegFrameGrabber(input);
grabber.start();
OpenCVFrameConverter.ToIplImage converter = new OpenCVFrameConverter.ToIplImage();
Frame frame = grabber.grab();
opencv_core.IplImage image = null;
if(frame == null) {
System.out.println("第一帧为空,请检查视频源");
return;
}
image = converter.convert(frame);
FrameRecorder recorder = FrameRecorder.createDefault(outFile, frame.imageWidth, frame.imageHeight);
recorder.setVideoCodec(AV_CODEC_ID_H264);
recorder.setFormat("flv");
recorder.setFrameRate(25);
recorder.setGopSize(25);
recorder.start();
Frame saveFrame;
while((frame = grabber.grab()) != null) {
saveFrame = converter.convert(image);
// 获取类型, 视频或者音频
// EnumSet<Frame.Type> videoOrAudio = saveFrame.getTypes();
// 录制视频
recorder.record(saveFrame);
}
recorder.close();
}
复制代码
七、运行截图
运行完成后本地成功生成了下载的视频文件
八、作者心得
我们生在一个技术百花齐放,日新月异的年代,生于这个时代即是幸运也是悲哀,在如此浩瀚无穷无尽的知识更迭浪潮中,很难保证全能,尽善尽美;有的人专注于算法,有的人专注与数据处理,还有的人动手能力不行,但是理论能力极强,比如著名物理学家杨振宁,有的人专注于如何应用落地,致力于将技术应用于社会生活,所以,如果本文对你有用,请不吝赞赏,如果你感觉内容过于浅薄或者是令你感到不适,也请缄默不言,互相留一份体面。
评论