asp.net 单个HLS段的按需无缝转码

ej83mcc0  于 12个月前  发布在  .NET
关注(0)|答案(2)|浏览(151)

后台

我一直想实现某些视频格式的按需转码,如“. mpeg”、“.wmv”、“.mov”等,以便使用ASP.NET Core 6.0C#ffmpeg在媒体管理服务器上提供这些视频格式。

我的方法

我决定使用的方法是提供一个动态生成的.m3u8文件,它只是使用一个选择的片段持续时间(例如10秒)和已知的视频持续时间生成。下面是我是如何做到的。请注意,分辨率目前没有实现并被丢弃:

public string GenerateVideoOnDemandPlaylist(double duration, int segment)
{
   double interval = (double)segment;
   var content = new StringBuilder();

   content.AppendLine("#EXTM3U");
   content.AppendLine("#EXT-X-VERSION:6");
   content.AppendLine(String.Format("#EXT-X-TARGETDURATION:{0}", segment));
   content.AppendLine("#EXT-X-MEDIA-SEQUENCE:0");
   content.AppendLine("#EXT-X-PLAYLIST-TYPE:VOD");
   content.AppendLine("#EXT-X-INDEPENDENT-SEGMENTS");

   for (double index = 0; (index * interval) < duration; index++)
   {
      content.AppendLine(String.Format("#EXTINF:{0:#.000000},", ((duration - (index * interval)) > interval) ? interval : ((duration - (index * interval)))));
      content.AppendLine(String.Format("{0:00000}.ts", index));
   }

   content.AppendLine("#EXT-X-ENDLIST");

   return content.ToString();
}

[HttpGet]
[Route("stream/{id}/{resolution}.m3u8")]
public IActionResult Stream(string id, string resolution)
{
   double duration = RetrieveVideoLengthInSeconds();
   return Content(GenerateVideoOnDemandPlaylist(duration, 10), "application/x-mpegURL", Encoding.UTF8);
}

字符串
下面是.m3u8文件的示例:

#EXTM3U
#EXT-X-VERSION:6
#EXT-X-TARGETDURATION:10
#EXT-X-MEDIA-SEQUENCE:0
#EXT-X-PLAYLIST-TYPE:VOD
#EXT-X-INDEPENDENT-SEGMENTS
#EXTINF:10.000000,
00000.ts
#EXTINF:3.386667,
00001.ts
#EXT-X-ENDLIST


因此,玩家会要求00000.ts00001.ts等,下一步是按需生成它们:

public byte[] GenerateVideoOnDemandSegment(int index, int duration, string path)
{
   int timeout = 30000;
   int totalWaitTime = 0;
   int waitInterval = 100;
   byte[] output = Array.Empty<byte>();
   string executable = "/opt/homebrew/bin/ffmpeg";
   DirectoryInfo temp = Directory.CreateDirectory(System.IO.Path.Combine(System.IO.Path.GetTempPath(), System.IO.Path.GetRandomFileName()));
   string format = System.IO.Path.Combine(temp.FullName, "output-%05d.ts");

   using (Process ffmpeg = new())
   {
      ffmpeg.StartInfo.FileName = executable;

      ffmpeg.StartInfo.Arguments = String.Format("-ss {0} ", index * duration);
      ffmpeg.StartInfo.Arguments += String.Format("-y -t {0} ", duration);
      ffmpeg.StartInfo.Arguments += String.Format("-i \"{0}\" ", path);
      ffmpeg.StartInfo.Arguments += String.Format("-c:v libx264 -c:a aac ");
      ffmpeg.StartInfo.Arguments += String.Format("-segment_time {0} -reset_timestamps 1 -break_non_keyframes 1 -map 0 ", duration);
      ffmpeg.StartInfo.Arguments += String.Format("-initial_offset {0} ", index * duration);
      ffmpeg.StartInfo.Arguments += String.Format("-f segment -segment_format mpegts {0}", format);

      ffmpeg.StartInfo.CreateNoWindow = true;
      ffmpeg.StartInfo.UseShellExecute = false;
      ffmpeg.StartInfo.RedirectStandardError = false;
      ffmpeg.StartInfo.RedirectStandardOutput = false;

      ffmpeg.Start();

      do
      {
         Thread.Sleep(waitInterval);
         totalWaitTime += waitInterval;
      }
      while ((!ffmpeg.HasExited) && (totalWaitTime < timeout));

      if (ffmpeg.HasExited)
      {
         string filename = System.IO.Path.Combine(temp.FullName, "output-00000.ts");

         if (!File.Exists(filename))
         {
            throw new FileNotFoundException("Unable to find the generated segment: " + filename);
         }

         output = File.ReadAllBytes(filename);
      }
      else
      {
         // It's been too long. Kill it!
         ffmpeg.Kill();
      }
   }

   // Remove the temporary directory and all its contents.
   temp.Delete(true);

   return output;
}

[HttpGet]
[Route("stream/{id}/{index}.ts")]
public IActionResult Segment(string id, int index)
{
   string path = RetrieveVideoPath(id);
   return File(GenerateVideoOnDemandSegment(index, 10, path), "application/x-mpegURL", true);
}


正如你所看到的,下面是我用来生成每个段的命令,每个段的-ss和-initial_offset递增10:

ffmpeg -ss 0 -y -t 10 -i "video.mov" -c:v libx264 -c:a aac -segment_time 10 -reset_timestamps 1 -break_non_keyframes 1 -map 0 -initial_offset 0 -f segment -segment_format mpegts /var/folders/8h/3xdhhky96b5bk2w2br6bt8n00000gn/T/4ynrwu0q.z24/output-%05d.ts

问题

在功能层面上,这些功能都可以正常工作,但是片段之间的过渡会出现一些小问题,尤其是音频在每10秒的时间内会出现非常短的中断。我如何确保片段是无缝的?在这个过程中我可以改进什么?

wwtsj6pe

wwtsj6pe1#

由于您使用的是段复用器,因此输入的持续时间应该是所需段的总和。
对于段00002.ts到00004.ts,
第一个月
(you将为每个连续的输出段集只运行一个命令)

4ngedf3f

4ngedf3f2#

  • 注意:我会听从@Gyan的意见.他是FFmpeg的首选,但我想给予一些实用的说明和替代方案。

我不认为你能够在实际中搜索到源视频并最终获得准确的独立片段转码。特别是如果你无法控制你正在转码的文件,或者当它们是各种格式时。
以下是我使用的几种按需转码方法:

选项A:只从头转码,不找

如果您的输出将被多个客户端重用,这是一个很好的选择。转码一次,缓存输出,你就可以开始了。你可以在同一个FFmpeg进程中为ABR处理多个比特率。
当然,缺点是,如果您的客户希望视频更远的未来,这将需要一些时间的转码器,以获得客户端想要的部分。

选项B:查找源,仅限一个客户端

在这种情况下,您可以在客户端需要的源中查找并流回结果,但 * 不要 * 假设任何先前的段都将排队。因此,除非您有多个客户端发出完全相同的请求,否则缓存输出几乎没有意义。
此外,如果有帮助的话,你不需要使用HLS。因为你是流回到一个客户端,你知道你想要什么比特率,你可以简单地从FFmpeg发送STDOUT到客户端播放。不需要写出段。

选项A + B

实际上,你可能需要使用这两种方法。如果有一个转码请求,你可以从请求的点开始流,同时排队后台作业来转码整个文件。

相关问题