返回

MP4到流视频转换

ffmpeg的流视频HLS格式转换

目录

FFmpeg 是一套可以用来记录、转换数字音频、视频,并能将其转化为流的开源计算机程序。它提供了录制、转换以及流化音视频的软件。

官方下载网站 http://www.ffmpeg.org/download.html 下载解压缩后配置环境

# MP4 转 M3U8

M3U8 是 Unicode 版本的 M3U,用 UTF-8 编码。”M3U” 和 “M3U8” 文件都是苹果公司使用的 HTTP Live Streaming(HLS) 协议格式的基础,这种协议格式可以在 iPhone 和 Macbook 等设备播放。

简单来说,m3u8是一个视频格式,就是将一个视频分成很多的小部分,这样方便视频的加载。

# 操作简单,效率低

1
ffmpeg -i input.mp4 -c:v libx264 -c:a aac -strict -2 -f hls -hls_list_size 2 -hls_time 15 output.m3u8

生成的效果是:

将 input.mp4 视频文件每 15 秒生成一个 ts 文件,最后生成一个 m3u8 文件,m3u8 文件是 ts 的索引文件。

我们直接用 VLC media player 等播放软件是可以直接打开 m3u8 文件,像播放 mp4 一样。

默认的每片长度为 2 秒,m3u8 文件中默认只保存最新的 5 条片的信息,导致最后播放的时候只能播最后的一小部分(直播的时候特别注意)。
-hls_time n 设置每片的长度,默认值为 2,单位为秒。
-hls_list_size n 设置播放列表保存的最多条目,设置为 0 会保存有所片信息,默认值为5。
-hls_wrap n 设置多少片之后开始覆盖,如果设置为0则不会覆盖,默认值为0。这个选项能够避免在磁盘上存储过多的 片,而且能够限制写入磁盘的最多的片的数量。
-hls_start_number n 设置播放列表中 sequence number 的值为 number,默认值为 0。
注意:播放列表的 sequence number 对每个 segment 来说都必须是唯一的,而且它不能和片的文件名(当使用 wrap 选项时,文件名有可能会重复使用)混淆。

更多参数请看文档:ffmpeg.org/ffmpeg.html#Video-Options

# 效率优化,提升效率

TS 文件是一种媒体的扩展名,它是日本高清摄像机拍摄下进行的封装格式。MPEG2-TS(Transport Stream“传输流”;又称TS、TP、MPEG-TS 或 M2T)是用于音效、图像与数据的通信协定,最早应用于DVD的实时传送节目。MPEG2-TS格式的特点就是要求从视频流的任一片段开始都是可以独立解码的。

1
2
3
4
# 1.视频整体转码ts
ffmpeg -y -i music.mp4  -vcodec copy -acodec copy -vbsf h264_mp4toannexb out\music.ts
# 2. .ts文件切片
ffmpeg -i music.ts -c copy -map 0 -f segment -segment_list out\music.m3u8 -segment_time 10 out\15s_%3d.ts

# hls_time 切片时间不准确的问题

播放 m3u8 的 ts 切片,必须要完整的下载一个 ts 切片,才能够播放,设置hls_time 的时间间隔越短越好( 根据实际情况来 ),实际过程中设置切片时间间隔为 2 秒,调用如下指令:

1
ffmpeg -i test.mp4 -c:v libx264 -c:a aac -strict -2 -f hls -hls_time 2 index.m3u8

但没有按照参数输入,进行切片。

原因:

ts 文件的切割,还跟原文件视频的 GOP 大小有关系(也就是两个 I 帧之间的时间间隔),因为任何一个 ts 分片第一帧必须是I帧,否则无法最快播放,并且第一帧不是 I 帧,对于播放器也是没有任何的意义,直接被播放器扔掉。任何一个视频流必须在获取到第一个I帧才能成功解码出图片。虽然指定了 1 秒切割一个 ts 文件,实际上,由于原视频流可能好几秒才有一个 I 帧,所以必须等到下一个 I 帧,才会重新开始切片。

解决:

既然知道要1秒产生一个ts分片,那就必须产生切片的过程中,强制一秒中产生一个关键帧。

设置关键帧间隔,设置间隔为 2 秒的参数如下:-force_key_frames "expr:gte(t,n_forced*2)

完整指令如:

1
ffmpeg -i test.mp4 -force_key_frames "expr:gte(t,n_forced*2)" -strict -2 -c:a aac -c:v libx264 -hls_time 2 -f hls index.m3u8

# m3u8 格式解析

完整的 m3u8 文件有三部分:

  • index.m3u8,保存视频的基本信息和分段文件顺序;
  • key,如果视频加密,保存密钥;
  • data文件,其他都是视频的数据文件。

具体内容解析:

  • #EXTM3U,是文件开始
  • #EXT-X-VERSION,标识HLS的协议版本号;
  • #EXT-X-TARGETDURATION,表示每个视频分段最大的时长(单位秒);
  • #EXT-X-MEDIA-SEQUENCE,表示播放列表第一个 URL 片段文件的序列号;
  • #EXT-X-PLAYLIST-TYPE,表明流媒体类型;
  • #EXT-X-KEY,加密方式,这里加密方式为AES-128,同时指定IV,在解密时需要;
  • #EXTINF,表示其后 URL 指定的媒体片段时长(单位为秒)。

# HLS

HLS 的工作原理是把整个流分成一个个小的基于 HTTP 的文件来下载,每次只下载一些。

当媒体流正在播放时,客户端可以选择从许多不同的备用源中以不同的速率下载同样的资源,允许流媒体会话适应不同的数据速率。

在开始一个流媒体会话时,客户端会下载一个包含元数据的 extended M3U (m3u8) playlist文件,用于寻找可用的媒体流。

HLS 只请求基本的 HTTP 报文,与实时传输协议(RTP)不同,HLS 可以穿过任何允许 HTTP 数据通过的防火墙或者代理服务器。

它也很容易使用内容分发网络来传输媒体流。

# m3u8(ts) 合并为 MP4

# 远程文件

1
ffmpeg -i “https://xpdbk.com/demo/ffmpeg/hls265/output.m3u8” -vcodec copy -acodec copy -absf aac_adtstoasc output.mp4

# 本地文件

1、打开 cmd

2、输入指令,按照文件的实际路径合并

合并成 ts文件 copy /b  F:\f\*.ts  E:\f\new.ts

合并成 MP4 文件 copy /b  F:\f\*.ts  E:\f\new.MP4

而通过 ffmpeg 命令如下:

1
2
3
4
5
6
7
8
直接转:  
ffmpeg -i new.ts -c copy -map 0:v -map 0:a output.mp4

指定音频流(一般用这个):  
ffmpeg -i new.ts -c copy -map 0:v -map 0:a -bsf:a aac_adtstoasc output.mp4

重编码视频:  
ffmpeg -y -i new.ts -c:v libx264 -c:a copy -bsf:a aac_adtstoasc output.mp4

# php实现代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
$url = 'https://******.m3u8?Expires=15853412145&OSSAccessKeyId=******&Signature=******';

$ts_content = file_get_contents($url);
$ts_content = explode(',', $ts_content);
$ts_file = array();

foreach ($ts_content as $key => $value) {
    if($key == 0) continue;
    $value = trim($value);
    $ts_file[] = substr($value, 0, strpos($value, '.ts') + 3);
}

$url_prefix = substr($url, 0, strpos($url, '.m3u8'));
$url_prefix = substr($url, 0, strrpos($url, '/') + 1);
$file_content = '';

foreach ($ts_file as $key => $value) {
    $file_content .= file_get_contents($url_prefix . $value);
}

file_put_contents('tmp_out.ts', $file_content);

// FFMPEG_PATH 是你自己解压ffmpeg的bin路径,例如我的是F:/ffmpeg/bin/
exec(FFMPEG_PATH . "ffmpeg -i tmp_out.ts tmp_out.mp4");

# Python实现代码

目录结构
./
  |– m3u8.py
  |– result
  |– 文件1
    |– key
    |– index.m3u8
    |– data…
  |– 文件2
    |– … 114514

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
import os
import sys
import time
from Crypto.Cipher import AES

def fileList(findex):
    rpath = os.path.dirname(os.path.realpath(findex))
    name = rpath.split("\\")[-1]
    fi = open(findex, 'r')
    flag = False
    IV = None
    tl = []
    for line in fi.readlines():
        if line.startswith("#EXT-X-KEY"):
            # 如果存在 IV 则提取;
            if line.split(",")[-1].startswith("IV="):
                IV = line.split(",")[-1][5:]
                IV = bytes.fromhex(IV)
        if line.startswith("#EXTINF"):
            flag = not flag
            continue
        if flag:
            tmp = line.strip().split("/")[-1]
            tmp = os.path.join(rpath, tmp)
            tl.append(tmp)
            flag = not flag
    fi.close()
    fk = open(os.path.join(rpath, "key"), 'rb')
    key = fk.read()
    fk.close()
    return name, tl, key, IV

def aes_decode(data, key, IV):
    # 如果没有指定 IV 值,则直接使用 key 值
    if not IV:
        IV = key
    cryptor = AES.new(key, AES.MODE_CBC, IV)
    plain_text = cryptor.decrypt(data)
    return plain_text

def main():
    fp = os.listdir()
    used = [s[:-4] for s in os.listdir("./result/")]
    for ind in fp:
        if not ind.isdigit():
            continue
        if ind in used:
            continue
        try: 
            name, fl, key, IV = fileList(os.path.join(ind, "index.m3u8"))
        except:
            print("-"*30)
            print("[-] Errot! file: ", ind)
            print("-"*30)
            continue
        print("[*] Begin process file: ", name)
        start = time.time()
        f = open(os.path.join("./result/", name+".mp4"), 'ab')
        for i in fl:
            with open(i, 'rb') as inf:
                data = inf.read()
                f.write(aes_decode(data, key, IV))
        f.close()
        print("[+] Sucessfully! Cost time: ", time.time()-start)

main()

# Golang实现代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
package main

import (
	"io/ioutil"
	"net/http"
	"os/exec"
	"strings"
)

func main() {
	url := "https://******.m3u8?Expires=15853412145&OSSAccessKeyId=******&Signature=******"

	resp, _ := http.Get(url)
	body, _ := ioutil.ReadAll(resp.Body)
	tsContent := strings.Split(string(body), ",")
	var tsFile []string

	for key, value := range tsContent {
		if key == 0 {
			continue
		}
		value = strings.TrimSpace(value)
		tsFile = append(tsFile, value[:strings.Index(value, ".ts")+3])
	}

	urlPrefix := url[:strings.LastIndex(url, "/")+1]
	var fileContent string

	for _, value := range tsFile {
		resp, _ := http.Get(urlPrefix + value)
		body, _ := ioutil.ReadAll(resp.Body)
		fileContent += string(body)
	}

	ioutil.WriteFile("tmp_out.ts", []byte(fileContent), 0644)

	cmd := exec.Command("ffmpeg", "-i", "tmp_out.ts", "tmp_out.mp4")
	cmd.Run()
}

# Rust实现代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
use std::fs::File;
use std::io::prelude::*;
use std::process::Command;
use reqwest;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let url = "https://******.m3u8?Expires=15853412145&OSSAccessKeyId=******&Signature=******";
    let resp = reqwest::get(url).await?.text().await?;
    let ts_content: Vec<&str> = resp.split(',').collect();
    let mut ts_file = vec![];

    for (i, line) in ts_content.iter().enumerate() {
        if i == 0 { continue; }
        let value = line.trim();
        ts_file.push(&value[0..value.find(".ts").unwrap()+3]);
    }

    let url_prefix = &url[0..url.rfind('/').unwrap()+1];
    let mut file_content = vec![];

    for file in ts_file {
        let resp = reqwest::get(&(url_prefix.to_string() + file)).await?.bytes().await?;
        file_content.extend_from_slice(&resp);
    }

    let mut file = File::create("tmp_out.ts")?;
    file.write_all(&file_content)?;

    Command::new("ffmpeg")
        .arg("-i")
        .arg("tmp_out.ts")
        .arg("tmp_out.mp4")
        .output()
        .expect("Failed to execute command");

    Ok(())
}

# C++实现代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
#include <iostream>
#include <fstream>
#include <string>
#include <vector>
#include <curl/curl.h>

size_t WriteCallback(void* contents, size_t size, size_t nmemb, std::string* s) {
    size_t newLength = size*nmemb;
    size_t oldLength = s->size();
    try {
        s->resize(oldLength + newLength);
    }
    catch(std::bad_alloc &e) {
        return 0;
    }

    std::copy((char*)contents,(char*)contents+newLength,s->begin()+oldLength);
    return size*nmemb;
}

std::string GetFile(const std::string& url) {
    CURL* curl;
    CURLcode res;
    std::string readBuffer;

    curl_global_init(CURL_GLOBAL_DEFAULT);
    curl = curl_easy_init();

    if(curl) {
        curl_easy_setopt(curl, CURLOPT_URL, url.c_str());
        curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, WriteCallback);
        curl_easy_setopt(curl, CURLOPT_WRITEDATA, &readBuffer);
        res = curl_easy_perform(curl);

        if(res != CURLE_OK)
            fprintf(stderr, "curl_easy_perform() failed: %s\n", curl_easy_strerror(res));

        curl_easy_cleanup(curl);
    }

    curl_global_cleanup();
    return readBuffer;
}

int main() {
    std::string url = "https://******.m3u8?Expires=15853412145&OSSAccessKeyId=******&Signature=******";
    std::string ts_content = GetFile(url);

    std::vector<std::string> ts_file;
    std::string delimiter = ",";
    size_t pos = 0;
    std::string token;

    while ((pos = ts_content.find(delimiter)) != std::string::npos) {
        token = ts_content.substr(0, pos);
        ts_file.push_back(token.substr(0, token.find(".ts") + 3));
        ts_content.erase(0, pos + delimiter.length());
    }

    std::string url_prefix = url.substr(0, url.rfind('/') + 1);
    std::ofstream outfile ("tmp_out.ts",std::ofstream::binary);

    for (auto &value : ts_file) {
        std::string file_content = GetFile(url_prefix + value);
        outfile.write(file_content.c_str(), file_content.size());
    }

    outfile.close();

    system("ffmpeg -i tmp_out.ts tmp_out.mp4");
}

# NASM实现代码

1
我不会

# NODEJS实现代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
const https = require('https');
const fs = require('fs');
const { exec } = require('child_process');

const url = 'https://******.m3u8?Expires=15853412145&OSSAccessKeyId=******&Signature=******';

https.get(url, (res) => {
    let tsContent = '';

    res.on('data', (chunk) => {
        tsContent += chunk;
    });

    res.on('end', () => {
        const tsFile = tsContent.split(',').slice(1).map(value => value.trim().split('.ts')[0] + '.ts');
        const urlPrefix = url.split('.m3u8')[0];

        let fileContent = '';

        tsFile.forEach((value, i) => {
            https.get(urlPrefix + value, (res) => {
                let data = '';

                res.on('data', (chunk) => {
                    data += chunk;
                });

                res.on('end', () => {
                    fileContent += data;

                    if (i === tsFile.length - 1) {
                        fs.writeFile('tmp_out.ts', fileContent, (err) => {
                            if (err) throw err;

                            exec('ffmpeg -i tmp_out.ts tmp_out.mp4', (err, stdout, stderr) => {
                                if (err) throw err;
                            });
                        });
                    }
                });
            });
        });
    });
});

# kotlin实现代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import java.io.*;
import java.net.URL;
import java.nio.file.*;
import java.util.stream.*;

public class Main {
    public static void main(String[] args) throws IOException {
        String url = "https://******.m3u8?Expires=15853412145&OSSAccessKeyId=******&Signature=******";

        String tsContent = new String(Files.readAllBytes(Paths.get(url)));
        String[] tsFile = tsContent.split(",");

        String urlPrefix = url.substring(0, url.lastIndexOf('/') + 1);
        StringBuilder fileContent = new StringBuilder();

        for (int i = 1; i < tsFile.length; i++) {
            String value = tsFile[i].trim();
            fileContent.append(new String(Files.readAllBytes(Paths.get(urlPrefix + value.substring(0, value.indexOf(".ts") + 3)))));
        }

        Files.write(Paths.get("tmp_out.ts"), fileContent.toString().getBytes());

        try {
            ProcessBuilder pb = new ProcessBuilder("ffmpeg", "-i", "tmp_out.ts", "tmp_out.mp4");
            Process p = pb.start();
            p.waitFor();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}