使用 Bento4、FFMPEG 和 BuyDRM 的视频编码器 DRM

什么是 DRM

DRM(数字版权管理)是一种控制和管理版权资料访问的技术,在我们的例子中就是视频。因此,你可能已经注意到,如果你尝试截图,或者在观看 Netflix 时共享屏幕,共享的屏幕或截图只会是黑屏。这是 DRM 的功能之一。因此,DRM 流程的基本高级视图是这样的。

使用 Bento4、FFMPEG 和 BuyDRM 的视频编码器 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

(0)

相关推荐

发表回复

登录后才能评论