以前看视频的时候,直接找到 video标签,查看视频地址,然后下载下来。。
后来发现,好多 video 标签打开元素审查,如下:
blob开始的东西,下载不了啦。。。
其实我们打开 network 还是能看见,加载了一堆的 .ts 文件。其实.ts文件就是被切成一段一段的视频。 理论上,把这些文件都下载下来,再合并,就完成了,,,
利用nodejs,request包 定时爬去 网站视频ts接口,大概有1771个文件。
首先,获取到网站的ts视频分段配置文件,获取到后,放入本地文件,方便下次使用。
然后,定时调用下载函数,进行下载,
爬去过程中会有下载失败的,所有我在爬去完毕后,检查下载失败的,再次进行下载,
exec包执行cmd命令 进行合成一个ts文件
最后,理论一句话,代码上千行...
一、问题
1、ts文件到底有多少和,地址从哪来。。。
答案: ts 相关的信息,都存在一个叫 m3u8 的文件。 如果仔细点观察 network 是可以找到这个文件的请求的。该文件内容大致如下:
这个文件,很显然,存了每个 ts 的文件名称,当然也有存完整的地址的。。只需要提取出里面的ts文件名称,再加上目标网站的域名,就可以下载了。。
我这里是手动的把 m3u8 下载到了本地,当然也可以自己写脚本来下载m3u8文件
解析代码如下:
const fs = require("fs");
var source = fs.readFileSync("./test.m3u8","utf-8"); //读取 m3u8
var arr = source.split("\n");
arr = arr.filter((item)=>{
return item.match(/\.ts$/);
});
2、使用什么技术来合并这些 ts
这里我尝试了两种办法
第一种: 使用node js 直接读取文件流,合并到一个文件。。。最后结果,合并确实成功了,也能播放,但是有卡顿现象,应该是视频帧被破坏了。
第二种: 使用一款强大的工具, ffmpeg 来合并,成功了。具体 ffmpeg 安装看这里 :https://www.cnblogs.com/xswl/p/10042195.html
其中 ffmpeg 的视频合成指令,我找到到了三类:
ffmpeg -i
"concat:1.ts|2.ts"
-acodec copy out.mp3
ffmpeg -i
"concat:1.ts|2.ts"
-acodec copy -vcodec copy -absf aac_adtstoasc output.mp4
前两类,都是要文件名称拼接到 指令里面,,考虑到 cmd 指令的长度有限制,所以并未采用。
采用了如下文件输入办法:
ffmpeg -i input.txt -acodec copy -vcodec copy -absf aac_adtstoasc output.mp4
其中 input.txt 是一个输入配置文件,内容为需要合并的文件名称,如下:
ffconcat version 1.0
file 0.ts
file 1.ts
二、正式开始
新建 down.js 写入:
const request = require("request");
const fs = require("fs");
const path = require("path");
const child_process = require('child_process');
const fsextra = require('fs-extra');
module.exports = function(opt){
opt = opt || {};
var arr = opt.arr || []; //所有 ts的文件名或者地址
var host = opt.host || ""; //下载 ts 的 域名,如果 arr 里面的元素已经包含,可以不传
var outputName = opt.name || `output${(new Date()).getTime()}.mp4`; //导出视频的名称
const tsFile = path.join(__dirname,`./source/${arr[0].split(".")[0]}`,);
createDir(tsFile);//递归创建文件
console.log("本次资源临时文件:",tsFile);
const resultDir = path.join(__dirname,"./result");
createDir(resultDir);//递归创建文件
const resultFile = path.join(resultDir,outputName);
var localPath = [] ; //下载到本地的路径
//开始下载ts文件
load();
function load(){
if(arr.length > 0){
var u = arr.shift();
var url = host + u;
console.log("progress---:",url);
down(url);
}else{
//下载完成
console.log("下载完成--开始生成配置");
localPath.unshift("ffconcat version 1.0");
try{
fs.writeFileSync(path.join(tsFile,"./input.txt"), localPath.join("\n") , undefined, 'utf-8')
}catch(e){
console.log("写入配置出错--",e);
return ;
}
//开始依赖配置合成
console.log("开始合成-----");
child_process.exec(`cd ${tsFile} && ffmpeg -i input.txt -acodec copy -vcodec copy -absf aac_adtstoasc ${resultFile}`,function(error, stdout, stderr){
if(error){
console.error("合成失败---",error);
}else{
console.log("合成成功--",stdout);
//删除临时文件
fsextra.remove(tsFile, err => {
if (err) return console.error("删除文件是失败",err)
console.log('删除文件成功!')
});
}
});
}
}
//下载 ts 文件
function down(url){
var p = url.split("?")[0];
var nm = path.parse(p);
var nme = nm["name"] + nm["ext"];
rpath = path.join(tsFile,nme);
localPath.push(`file ${nme}`); //缓存本地路径,用来合成
request({
url:url,
headers:{
'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.98 Safari/537.36',
'X-Requested-With': 'XMLHttpRequest'
}
},function (err, response, body) {
if (!err && response.statusCode == 200) {
load();
}else{
console.log("错误",err)
}
}).pipe(fs.createWriteStream(rpath));
}
//递归的创建文件夹
function mkdirs(dirpath) {
if (!fs.existsSync(path.dirname(dirpath))) {
mkdirs(path.dirname(dirpath));
}
fs.mkdirSync(dirpath);
}
function createDir(myPath){
fs.existsSync(myPath) == false && mkdirs(myPath);
}
}
//ffmpeg -i "concat:1.ts|2.ts" -acodec copy out.mp3
//ffmpeg -i "concat:1.ts|2.ts" -acodec copy -vcodec copy -absf aac_adtstoasc output.mp4
// ffmpeg -i input.txt -acodec copy -vcodec copy -absf aac_adtstoasc output.mp4
/*
ffconcat version 1.0
file 0.ts
file 1.ts
*/
/*
//文件移动
function moveFile(oldPath,newPath){
try {
fs.renameSync(oldPath, newPath);
}
catch (e) {
console.log("报错后强制移动",e);
fs.renameSync(oldPath, newPath);
}
}
*/
线程下载
const request = require('request')
const fs = require('fs')
const path = require('path')
const child_process = require('child_process')
const fsextra = require('fs-extra')
module.exports = function(opt) {
opt = opt || {}
var arr = opt.arr || [] // 所有 ts的文件名或者地址
var headers = opt.headers || { // 请求头
'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.98 Safari/537.36',
'X-Requested-With': 'XMLHttpRequest'
}
var host = opt.host || '' // 下载 ts 的 域名,如果 arr 里面的元素已经包含,可以不传
var outputName = opt.name || `output${(new Date()).getTime()}.mp4` // 导出视频的名称
const tsFile = path.join(__dirname, `./source/${arr[0].split('.')[0]}`)
createDir(tsFile)// 递归创建文件
console.log('本次资源临时文件:', tsFile)
const resultDir = path.join(__dirname, './result')
createDir(resultDir)// 递归创建文件
const resultFile = path.join(resultDir, outputName)
var localPath = [] // 下载到本地的路径
// 开始下载ts文件
load()
function load() {
var task = []
var taskCount = 10 // 线程下载
var flag = false
for (let i = 0; i < taskCount; i++) {
var u = arr.shift()
if (!u) {
flag = true
break
}
var url = host + u
console.log('progress---:', url)
task.push(downPromise(url, headers))
}
Promise.all(task).then(function() {
if (flag) {
downOver()
} else {
load()
}
})
}
// 下载完成
function downOver() {
console.log('下载完成--开始生成配置')
localPath.unshift('ffconcat version 1.0')
try {
fs.writeFileSync(path.join(tsFile, './input.txt'), localPath.join('\n'), undefined, 'utf-8')
} catch (e) {
console.log('写入配置出错--', e)
return
}
// 开始依赖配置合成
console.log('开始合成-----')
child_process.exec(`cd ${tsFile} && ffmpeg -i input.txt -acodec copy -vcodec copy -absf aac_adtstoasc ${resultFile}`, function(error, stdout, stderr) {
if (error) {
console.error('合成失败---', error)
} else {
console.log('合成成功--', stdout)
// 删除临时文件
fsextra.remove(tsFile, err => {
if (err) return console.error('删除文件是失败', err)
console.log('删除文件成功!')
})
}
})
}
// 下载 ts 文件
function downPromise(url, headers) {
return new Promise(function(resolve, reject) {
var p = url.split('?')[0]
var nm = path.parse(p)
var nme = nm['name'] + nm['ext']
var rpath = path.join(tsFile, nme)
localPath.push(`file ${nme}`) // 缓存本地路径,用来合成
request({
url,
headers
}, function(err, response, body) {
if (!err && response.statusCode == 200) {
resolve(true)
} else {
console.log('错误', err)
reject(err)
}
}).pipe(fs.createWriteStream(rpath))
})
}
// 递归的创建文件夹
function mkdirs(dirpath) {
if (!fs.existsSync(path.dirname(dirpath))) {
mkdirs(path.dirname(dirpath))
}
fs.mkdirSync(dirpath)
}
function createDir(myPath) {
fs.existsSync(myPath) == false && mkdirs(myPath)
}
}
然后再新建 main.js
const fs = require("fs");
const down = require("./down");
var host = 'https://xxxx/'; //目标网站
var outputName = "output.mp4";
var source = fs.readFileSync("./test.m3u8","utf-8"); //读取 m3u8
var arr = source.split("\n");
arr = arr.filter((item)=>{
return item.match(/\.ts$/);
});
down({
arr,
host,
name:outputName
})
// host:https://youku.com-qq.net/20190502/181_7ffa42fa/1000k/hls/
// m3u8:https://youku.com-qq.net/20190502/181_7ffa42fa/1000k/hls/index.m3u8
里面使用到了 fs-extra 模块,所以先安装
npm i fs-extra
最后执行:
node main.js