文章目录
- 概要
- 如何解密M3U8文件呢
- 实现思路和代码
- 序列图
- 网络请求
- 解密
- 结论
概要
视频文件很多已M3U8文件格式来提供,先复习下什么是M3U8文件!用QT的 mutimedia框架来播放视频时,有的视频加载慢,有的视频加载快,为啥?结论再最后
m3u8 文件实质是一个播放列表(playlist),其可能是一个媒体播放列表(Media Playlist),或者是一个主列表(Master Playlist)。但无论是哪种播放列表,其内部文字使用的都是 utf-8 编码。
当 m3u8 文件作为媒体播放列表(Meida Playlist)时,其内部信息记录的是一系列媒体片段资源,顺序播放该片段资源,即可完整展示多媒体资源。其格式如下所示:
#EXTM3U
#EXT-X-TARGETDURATION:10
#EXTINF:9.009,
http://media.example.com/first.ts
#EXTINF:9.009,
http://media.example.com/second.ts
#EXTINF:3.003,
http://media.example.com/third.ts
对于点播来说,客户端只需按顺序下载上述片段资源,依次进行播放即可。而对于直播来说,客户端需要 定时重新请求 该 m3u8 文件,看下是否有新的片段数据需要进行下载并播放。
当 m3u8 作为主播放列表(Master Playlist)时,其内部提供的是同一份媒体资源的多份流列表资源(Variant Stream)。其格式如下所示:
#EXTM3U
#EXT-X-STREAM-INF:BANDWIDTH=150000,RESOLUTION=416x234,CODECS="avc1.42e00a,mp4a.40.2"
http://example.com/low/index.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=240000,RESOLUTION=416x234,CODECS="avc1.42e00a,mp4a.40.2"
http://example.com/lo_mid/index.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=440000,RESOLUTION=416x234,CODECS="avc1.42e00a,mp4a.40.2"
http://example.com/hi_mid/index.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=640000,RESOLUTION=640x360,CODECS="avc1.42e00a,mp4a.40.2"
http://example.com/high/index.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=64000,CODECS="mp4a.40.5"
http://example.com/audio/index.m3u8
该备用流资源指定了多种不同码率,不同格式的媒体播放列表,并且,该备用流资源也可同时提供不同版本的资源内容,比如不同语言的音频文件,不同角度拍摄的视屏文件等等。客户可以根据不同的网络状态选取合适码流的资源,并且最好根据用户喜好选择合适的资源内容。
以上,就是 m3u8 文件的大概内容
如何解密M3U8文件呢
示例:
M3U8文件是一种播放列表文件,用于存储和组织HLS(HTTP Live Streaming)流媒体数据。在M3U8文件中,EXT-X-KEY、URI和IV等字段是用于描述流媒体的关键信息。
EXT-X-KEY: 这个字段用于指定加密密钥的信息。它通常包含一个URI,该URI指向包含密钥的媒体文件。该字段还可能包含其他参数,如密钥的加密算法和密码等。
URI: 这个字段指定了媒体文件的URL地址。它用于告诉播放器从哪个位置获取媒体数据。
IV: 这个字段是初始化向量(Initialization Vector)的缩写,用于加密算法的初始化过程。在HLS流媒体中,每个媒体片段都使用不同的初始化向量进行加密,以确保每个片段的加密是独立的。
这些字段通常以特定的格式出现在M3U8文件中。下面是一个示例:
#EXTM3U
#EXT-X-STREAM-INF:PROGRAM-ID=1, BANDWIDTH=1280000
http://example.com/stream.m3u8?token=1234567890
#EXT-X-KEY:METHOD=AES-128, URI="http://example.com/key.txt", IV=0x00000000000000000000000000000001
#EXT-X-KEY:METHOD=AES-128, URI="http://example.com/key2.txt", IV=0x00000001000000010000000100000002
#EXT-X-I-FRAME-STREAM-INF:PROGRAM-ID=1, BANDWIDTH=512000, CODECS="mp4a.40.2", RESOLUTION=480x360, FRAME-RATE=15
#EXTINF:16.733333,
http://example.com/iframe.m3u8?token=abcdefghijklmnopqrstuvwxyz
在上述示例中,#EXTM3U标识了文件为M3U8播放列表的开始。#EXT-X-STREAM-INF指定了流媒体的信息,如节目ID和带宽。http://example.com/stream.m3u8?token=1234567890是媒体文件的URI。接下来的#EXT-X-KEY字段指定了加密密钥的信息,包括加密方法和密钥的URI以及初始化向量(IV)。在这个例子中,有两个密钥,每个密钥对应一个媒体片段。最后,#EXT-X-I-FRAME-STREAM-INF指定了I帧媒体流的信息,包括节目ID、带宽、编解码器、分辨率和帧率等。http://example.com/iframe.m3u8?token=abcdefghijklmnopqrstuvwxyz是I帧媒体文件的URI。
实现思路和代码
序列图
网络请求
QNetworkRequest request(url); //请求m3u8地址
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded");
addAllCookie(request);
QNetworkReply *pNetworkResponse = pManager->get(request);
m_tsContent.clear();
m_keyContent.clear();
m_ivData.clear();
QObject::connect(pNetworkResponse, &QNetworkReply::finished, [=]{
if (pNetworkResponse->error() == QNetworkReply::NoError)
{
QByteArray bytes = pNetworkResponse->readAll();
QJsonObject json_object = QJsonDocument::fromJson(bytes).object();
if(json_object["code"].toInt() == 10000)
{
if(!json_object["data"].isUndefined())
{
QJsonValue data = json_object["data"];
QJsonValue urls = data["videoUrl"];
// Test url here
QUrl videoUrl(urls["normal"].toString());
QNetworkRequest videoRequest(videoUrl);
videoRequest.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded");
m_elaptimer.restart();
QNetworkReply *pVideoNetworkResponse = pManager->get(videoRequest);
connect(pVideoNetworkResponse, &QNetworkReply::finished, [=]{
m3u8time = m_elaptimer.elapsed();
if(pVideoNetworkResponse->error() == QNetworkReply::NoError) {
QByteArray data = pVideoNetworkResponse->readAll();
QString m3u8Content = QString::fromUtf8(data);
QStringList lines = m3u8Content.split('\n');
QString keyLine;
QString tsLine;
foreach (const QString &line, lines) {
if (line.startsWith("#EXT-X-KEY:")) {
keyLine = line;
qDebug() << "[www]: keyline - " << keyLine;
} else if (line.startsWith("https:")) {
tsLine = line;
qDebug() << "[www]: tsLine - " << tsLine;
break;
qDebug() << "[www]: test fist snippet and stop";
}
}
QRegularExpression keyRegex("#EXT-X-KEY:METHOD=([A-Za-z0-9-]+),URI=\"([^\"]+)\",IV=([A-Za-z0-9-]+)");
QRegularExpressionMatchIterator matchIterator = keyRegex.globalMatch(keyLine);
QString method;
QString keyUri;
QString IVString;
if (matchIterator.hasNext()) {
QRegularExpressionMatch match = matchIterator.next();
method = match.captured(1);
keyUri = match.captured(2);
IVString = match.captured(3);
}
qDebug() << "[www] method: " << method;
qDebug() << "[www] keyUri: " << keyUri;
qDebug() << "[www] tsLine: " << tsLine;
qDebug() << "[www] IVString: " << IVString;
QByteArray tsData = QByteArray();
QByteArray keyData = QByteArray();
QByteArray ivData = QByteArray::fromHex(IVString.right(IVString.size() - 2).toLatin1());
m_ivData = ivData;
qDebug() << "[www] ivData: " << ivData;
QString keyContent;
QString tsContent;
//Get key from uri
QUrl keyUrl(keyUri);
QNetworkRequest keyQuest(keyUrl);
keyQuest.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded");
beginKey = m_elaptimer.elapsed();
QNetworkReply *keyResponse = pManager->get(keyQuest);
connect(keyResponse, SIGNAL(error(QNetworkReply::NetworkError)),
this, SLOT(httpError(QNetworkReply::NetworkError)));
connect(keyResponse, &QNetworkReply::finished, [=]{
if(keyResponse->error() == QNetworkReply::NoError) {
QByteArray keyData = keyResponse->readAll();
QString keyContent = QString::fromUtf8(keyData);
qDebug() << "[www] keyContent:" << keyContent;
qDebug() << "[www] keyData:" << keyData.toHex();
m_keyContent = keyData;
checkAesPara();
} else {
qDebug() << "[xiaole]" << keyResponse->errorString() ;
}
});
//Get ts data from uri
QUrl tsUrl(tsLine);
QNetworkRequest tsRequest(tsUrl);
tsRequest.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded");
QNetworkReply *tsResponse = pManager->get(tsRequest);
connect(tsResponse, SIGNAL(error(QNetworkReply::NetworkError)),
this, SLOT(httpError(QNetworkReply::NetworkError)));
connect(tsResponse, &QNetworkReply::finished, [=]{
if(tsResponse->error() == QNetworkReply::NoError) {
QByteArray tsData = tsResponse->readAll();
QString tsContent = QString::fromUtf8(tsData);
m_tsContent = tsData;
checkAesPara();
}
});
} else {
}
});
}
}
}
pNetworkResponse->close();
pNetworkResponse->deleteLater();
});
解密
#include "qaesencryption.h"
extern "C" {
#include <openssl/aes.h>
#include <openssl/rand.h>
QByteArray aesDecrypt(const QByteArray& cipherText, const QByteArray& key, const QByteArray& iv) {
// 创建一个AES解密上下文
AES_KEY decryptKey;
AES_set_decrypt_key(reinterpret_cast<const unsigned char*>(key.constData()), 128, &decryptKey);
// 解密数据
QByteArray decryptedText(cipherText.size(), Qt::Uninitialized);
AES_cbc_encrypt(reinterpret_cast<const unsigned char*>(cipherText.constData()),
reinterpret_cast<unsigned char*>(decryptedText.data()),
cipherText.size(),
&decryptKey,
const_cast<unsigned char*>(reinterpret_cast<const unsigned char*>(iv.data())),
AES_DECRYPT);
return decryptedText;
}
}
QByteArray qaesDecrypt(const QByteArray& cipherText, const QByteArray& key, const QByteArray& iv) {
return QAESEncryption::Decrypt(QAESEncryption::AES_128, QAESEncryption::CBC, cipherText, key,
iv, QAESEncryption::ZERO);
}
checkAesPara()
{
if(!m_tsContent.isEmpty() && !m_keyContent.isEmpty())
{
qDebug()<<"keyContent"<< m_keyContent <<"tsContent"<< m_tsContent.length();
QByteArray decTsData = aesDecrypt(m_tsContent, m_keyContent, m_ivData);
QFile file("/home/test.ts"); //保存到本地文件
if (file.open(QIODevice::ReadWrite | QIODevice::Truncate))
{
file.write(decTsData);
file.close();
}
writeOver = m_elaptimer.elapsed();
writeTime = writeOver-jiemiOver;
}
}
结论
`
● ts片段越大,缓冲时间越长,反之亦然
● qt播放器并不需要一个完整的ts下载完才开始播放。
所以视频加载快慢,最大的因素是网速,还有就是跟TS片段大小有关系。