什么是 DRM
DRM(数字版权管理)是一种控制和管理版权资料访问的技术,在我们的例子中就是视频。因此,你可能已经注意到,如果你尝试截图,或者在观看 Netflix 时共享屏幕,共享的屏幕或截图只会是黑屏。这是 DRM 的功能之一。因此,DRM 流程的基本高级视图是这样的。
使用苹果生态系统设备播放的媒体(HLS 流媒体)主要使用 Fairplay 作为 DRM 提供商,谷歌有 Widevine,微软有 play ready。在本教程中,我将介绍 Widevine,因为它是最流行的 DRM 服务。
DRM 有很多优点,但我更关注与我们项目相关的实施。在我们的项目中,我们将使用 BuyDRM 作为 DRM 许可证服务器提供商。这是一项付费服务。你必须与他们签订合同。也有一些免费试用的服务,其实施方法大致相同。
从 BuyDRM 创建密钥 ID、密钥和 Hash Header
为此,您必须首先使用 openSSL 创建私钥和公钥。
openssl req -x509 -newkey rsa:4096 -keyout private_key.pem -out public_cert.pem -nodes -days 1461 -subj "/C=<COUNTRY_CODE(Example: SG)>/O=YOURCOMPANYNAME/CN=COMMONNAME"
现在,要获取密钥 ID、密钥和 Hash Header,必须使用 KeyOS CPIX API。现在,相关代码尚未公开。遗憾的是,未经他们同意,我不能分享与 CPIX 端点相关的代码。不过,如果有人知道的话。只需将这些私钥和公钥与 buyDRM 提供的 CPIX 证书一起添加,然后运行 CPIX 提供的 javascript 代码即可。
现在你会得到一个错误提示:”发送授权未获授权”。原因是您需要向 buyDRM 注册您的公钥。因此,请联系您的销售代表或 buyDRM 的支持联系人,并将密钥交给他们进行注册。之后,当你运行 CPIX 代码时。你会得到如下结果:
[
{
cek: <KEY> ,
Kid: <KEY_ID> ,
playready: {
psshBase64: <HASH_HEADER> ,
psshHex: '<>'
}
},
{
cek: <KEY> ,
Kid: <KEY_ID> ,
Widevine: {
psshBase64: <HASH_HEADER> ,
psshHex: <>
}
}
]
执行
现在,让我们将这些值添加到编码器代码中。
const ffmpegStatic = require("ffmpeg-static");
const ffmpeg = require("fluent-ffmpeg");
const fs = require("fs");
const { exec } = require("child_process");
ffmpeg.setFfmpegPath(ffmpegStatic);
const bitrates = [
{
resolution: "1280x720",
videoBitrate: "1500k",
audioBitrate: "128k",
outputName: "output_720p.mp4",
},
{
resolution: "854x480",
videoBitrate: "500k",
audioBitrate: "96k",
outputName: "output_480p.mp4",
},
{
resolution: "640x360",
videoBitrate: "250k",
audioBitrate: "64k",
outputName: "output_360p.mp4",
},
];
const encodeVideo = (inputVideo, outputFolder, config) => {
return new Promise((resolve, reject) => {
ffmpeg(inputVideo)
.videoCodec("libx264")
.audioCodec("aac")
.videoBitrate(config.videoBitrate)
.audioBitrate(config.audioBitrate)
.size(config.resolution)
.output(`${outputFolder}/${config.outputName}`)
.on("end", () => {
console.log(`Finished encoding ${config.outputName}`);
resolve(`${outputFolder}/${config.outputName}`);
})
.on("error", (err) => {
console.error(`Error encoding ${config.outputName}: ${err}`);
reject(err);
})
.run();
});
};
const fragmentVideo = (inputVideo, outputFolder) => {
return new Promise((resolve, reject) => {
const strArr = inputVideo.split("/");
const fragmentedVideoFile = `${outputFolder}/fragmented_${strArr[1]}`;
const mp4fragmentCommand = `mp4fragment ${inputVideo} ${fragmentedVideoFile}`;
exec(mp4fragmentCommand, (error, stdout, stderr) => {
if (error) {
console.error(`Error running mp4fragment: ${error.message}`);
reject(stderr);
}
console.log(stdout);
resolve(fragmentedVideoFile);
});
});
};
const dashEncodeVideo = (fragmentedFiles, outputDirectory) => {
return new Promise((resolve, reject) => {
const mpdOutputFile = `${outputDirectory}/stream.mpd`;
let mp4dashCommand = `mp4dash --widevine-header "#<HASH_HEADER>" --encryption-key=<KEY_ID>:<KEY> --output-dir=${outputDirectory}`;
for (const fragmentedFile of fragmentedFiles) {
mp4dashCommand = mp4dashCommand + ` ${fragmentedFile}`;
}
exec(mp4dashCommand, (error, stdout, stderr) => {
if (error) {
console.error(`Error running mp4dash: ${error.message}`);
reject(stderr);
}
const mpdContent = fs.readFileSync(mpdOutputFile, "utf8");
const dashManifest = `<?xml version="1.0" encoding="utf-8"?>
<MPD xmlns="urn:mpeg:dash:schema:mpd:2011" minBufferTime="PT1.500S" profiles="urn:mpeg:dash:profile:isoff-live:2011" type="dynamic" mediaPresentationDuration="PT0H3M17.13S" maxSegmentDuration="PT0H0M4.800S">
${mpdContent}
</MPD>`;
const dashManifestFile = `${outputDirectory}/manifest.mpd`;
fs.writeFileSync(dashManifestFile, dashManifest);
resolve(dashManifest);
});
});
};
const encoder = async () => {
const inputVideo = "input.mp4";
const outputDirectory = "output_folder";
const outputDirectoryDash = "output_dash";
const outputDirectoryFragment = "output_fragment";
const permissions = 0o777;
try {
if (fs.existsSync(outputDirectory)) {
fs.rmSync(outputDirectory, { recursive: true });
}
fs.mkdirSync(outputDirectory);
fs.chmodSync(outputDirectory, permissions);
const encodingQueue = [];
for (const config of bitrates) {
encodingQueue.push(encodeVideo(inputVideo, outputDirectory, config));
}
const outPutVideoFiles = await Promise.all(encodingQueue);
console.log("All encoding tasks completed.");
if (fs.existsSync(outputDirectoryFragment)) {
fs.rmSync(outputDirectoryFragment, { recursive: true });
}
fs.mkdirSync(outputDirectoryFragment);
fs.chmodSync(outputDirectoryFragment, permissions);
const fragmentingQueue = [];
for (const bitrateFile of outPutVideoFiles) {
fragmentingQueue.push(
fragmentVideo(bitrateFile, outputDirectoryFragment)
);
}
const fragmentedFiles = await Promise.all(fragmentingQueue);
console.log("All fragmenting tasks completed.");
if (fs.existsSync(outputDirectoryDash)) {
fs.rmSync(outputDirectoryDash, { recursive: true });
}
const dashManifest = await dashEncodeVideo(
fragmentedFiles,
outputDirectoryDash
);
console.log("Encoding successfully completed");
console.log(dashManifest);
} catch (err) {
console.error("Error during encoding:", err);
}
};
encoder();
虽然整个代码很长,但我们只修改了下面一行。
let mp4dashCommand = `mp4dash --widevine-header "#<HASH_HEADER>" --encryption-key=<KEY_ID>:<KEY> --output-dir=${outputDirectory}`;
这里我们添加了上一步中的 Widevine 配置。请记住,Key ID 的格式与 UUID 格式类似,都带有连字符。使用 KeyID 值时,请去掉连字符。
现在运行编码器代码。输入的视频将在启用 DRM 后进行编码。
查看 DRM 内容
因此,我们需要更改视频播放器来查看这些内容。之前我使用的是普通的 dash js 插件,但在本教程中我将使用 videoJS 库。VideoJS 和 VideoJS-contrib-eme 支持启用 DRM 的内容。
<!DOCTYPE html>
<html lang="en">
<head>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/video.js/7.15.6/video.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/videojs-contrib-eme@3.9.0/dist/videojs-contrib-eme.min.js"></script>
<style>
.video-js, video {
width: 100%;
min-height: 640px;
}
</style>
</head>
<body>
<div class="container">
<div class="row">
<video id="my-player" class="video-js" controls></video>
</div>
<script type="text/javascript">
(function() {
var player = videojs('my-player');
player.eme({
emeHeaders: {
'customdata': <Authentication Key>
}
});
player.on('ready', function() {
var wvprDashSrc = {
src: 'stream.mpd',
type: 'application/dash+xml',
keySystems: {
'com.widevine.alpha': <KEY_OS_SERVER>
}
};
player.src(wvprDashSrc);
});
})();
</script>
</body>
</html>
在 eme 头文件中,我们必须向 buyDRM 提供的许可证服务器发送授权密钥。联系 buyDRM 团队以创建验证 xml 签名密钥,然后获取 auth 密钥。添加完成后,您的 DRM 加密视频就可以观看了。
最后,我个人认为本教程对于正在使用 BuyDRM 并积极开发生产级 VOD 系统的工程师非常有用。
参考资料:
https://buydrm.com/buydrm-releases-cpix-specation-v1-1-to-clients-and-esp-partners/
https://go.buydrm.com/thedrmblog/deploying-drm-workflows-with-ffmpeg
https://www.bento4.com/developers/dash/encryption_and_drm/
作者:Dimuthu Wickramanayake
来源:https://blog.stackademic.com/drm-with-bento4-ffmpeg-and-buydrm-694cfb80dcee
本文来自作者投稿,版权归原作者所有。如需转载,请注明出处:https://www.nxrte.com/jishu/47957.html