Генерация сегментов HLS TS с помощью C#C#

Место общения программистов C#
Ответить
Anonymous
 Генерация сегментов HLS TS с помощью C#

Сообщение Anonymous »

Пример файла H264
Пример исходного кода
В конце моего сообщения вы найдете код для моего примера проекта, пример файла элементарного потока h264 и файл вывод на экран. Целью этого примера проекта является потоковая передача данных, считанных из файла h264, в формате HLS (HTTP Live Stream).
Я написал такие части, как обработка запросов и создание списков воспроизведения. Однако мне не удалось полностью реализовать часть генерации сегментов (TS). Я теоретически понимаю, как это должно быть сделано, но здесь задействовано слишком много разных концепций. Например, создание 188-байтовых пакетов и добавление некоторых верхних или нижних колонтитулов и т. д.
Моя конечная цель — обслуживать данные элементарного потока h264, полученные из RTSP, как HLS. Поэтому мне даже не нужно поддерживать все форматы. Как вы знаете, данные h264 состоят из данных SPS, PPS, IFrame, PFrame, разделенных единицей NAL 0x00 00 00 01.
На самом деле существуют проекты, написанные на Go. которые выполняют эту задачу, но мне нужно сделать это на C#. Как вы можете видеть на скриншотах, которыми я поделился ниже, данные, передаваемые с помощью кода, написанного внутри GetBytes, можно просмотреть с помощью ffplay (ffplay http://localhost:8080/stream.m3u8).
Однако я полностью осознаю, что это неправильный подход. Метод GetBytes (или GetNextSegment, который вызывает GetBytes) должен генерировать сегменты TS, о которых я упоминал ранее. На самом деле его нельзя воспроизвести в проигрывателе VLC или интернет-браузерах.
В коде, которым я поделился ниже, я хотел бы обратить ваше внимание на файл H264FileToHLSServer.cs. .
Прошу вашей помощи в этом вопросе.
  • Как мне следует обращаться с исходными данными в GetSegment< /код> метод? Должен ли я читать его покадрово, как байтовые блоки фиксированной длины или в блоках SPS, PPS?
  • Как мне генерировать сегменты TS? Каков алгоритм этого действия?
Предпочитаю решить эту проблему, написав простой код на C#. Однако я также открыт для решений, использующих библиотеку C# ffplay.Autogen или кодировщик Microsoft MPEG-2. Я делюсь всем кодом ниже. Надеюсь, на этот раз мне помогут. Заранее спасибо.
Запуск сервера и вызов его с помощью ffplay:
[img]https: //i.sstatic.net/FMzC2yVo.png[/img]

Результат с ffplay:
Изображение

Вот мой код:
Program.cs:
var trace = (string message) => Console.WriteLine(DateTime.Now.ToString("HH:mm:ss.fff") + ": " + message);

var options = new FBCHLSServerOptions(8080, false)
{
LLPartTarget = .5f,
//LowLatencySupport = false,
//Port = 8080,
SegmentDuration = 4,
SegmentBufferCount = 5,
SourceFPS = 2,
SegmentRequestType = HLSSegmentRequestType.TS,
};

trace("Starting server...:");

//File path: d:\a\0_240p_2fps.264
var server = new H264FileToHLSServer(@"d:\a\0_240p_2fps.264", options);

server.OnTrace += (s, e) => trace(e);
server.Start();
trace("Server started. Press any key to stop.");

// http://localhost:8080/stream.m3u8
trace($"URL: http://localhost:{options.Port}/stream.m3u8");
Console.ReadKey();
server.Stop();
server.Dispose();
trace("Bye!");

FBCHLSServerOptions:
public enum HLSSegmentRequestType
{
TS,
M4S,
}

public class FBCHLSServerOptions
{
public int Port { get; set; }
///
/// Low latency support
///
public bool LowLatencySupport { get; set; }
public int SourceFPS { get; set; } = 25;
///
/// Segment duration in seconds
///
public int SegmentDuration { get; set; } = 4;
///
/// Segment buffer count. Meaning how many segments will be sent to client for each request
///
public int SegmentBufferCount { get; set; } = 5;
///
/// Low latency part target. This is the part of the segment that will be sent to client. Default is .5
///
public float LLPartTarget { get; set; } = .5f;

public HLSSegmentRequestType SegmentRequestType { get; set; } = HLSSegmentRequestType.TS;

public FBCHLSServerOptions(int port, bool lowLatencySupport)
{
Port = port;
LowLatencySupport = lowLatencySupport;
}

public override string ToString()
{
//Json with intended and with enum as string
return System.Text.Json.JsonSerializer.Serialize(this,
new System.Text.Json.JsonSerializerOptions
{
WriteIndented = true,
Converters = { new System.Text.Json.Serialization.JsonStringEnumConverter()
}
});
}
}

H264FileToHLSServer.cs:
public class H264FileToHLSServer : FBCHLSServerBase, IDisposable
{
private readonly string filePath;
private FileStream? fileStream;

public H264FileToHLSServer(string filePath, FBCHLSServerOptions options) : base(options)
{
this.filePath = filePath;
InitializeFileStream();
}

private void InitializeFileStream()
{
try
{
fileStream = new FileStream(filePath, FileMode.Open, FileAccess.Read);
}
catch (Exception ex)
{
Trace($"Error initializing file stream: {ex.Message}");
throw;
}
}

///
/// TODO: I know this part shouldn't be like this, but I don't know how to make Mpeg 2 TS.
///
///
///
private byte[] GetBytes(int frames)
{
if (fileStream == null)
{
InitializeFileStream();
}

byte[] buffer = new byte[frames * 1024];
var read = fileStream!.Read(buffer, 0, buffer.Length);

if (read == 0)
{
fileStream.Seek(0, SeekOrigin.Begin);
read = fileStream.Read(buffer, 0, buffer.Length);
}

return buffer.Take(read).ToArray();
}

public override byte[] GetInitSegment()
{
return GetBytes(1);
}

public override byte[] GetNextPart(int frames)
{
return GetBytes(frames);
}

public override byte[] GetNextSegment(int frames)
{
return GetBytes(frames);
}

public void Dispose()
{
fileStream?.Dispose();
}
}

FBCHLSServerBase.cs:
using System.Net;
using System.Text;

public abstract class FBCHLSServerBase
{
int segmentIndex = 1;
int partIndex = 0; // Added for LL-HLS part tracking
object lockObj = new object();
HttpListener? listener;
FBCHLSServerOptions options;

public event EventHandler? OnTrace;

protected void Trace(string message)
{
OnTrace?.Invoke(this, DateTime.Now.ToString("HH:mm:ss") + ": " + message);
}

public FBCHLSServerBase(FBCHLSServerOptions options)
{
this.options = options;
}

public void Start()
{
listener = new HttpListener();
listener.Prefixes.Add($"http://localhost:{options.Port}/");
listener.Start();
listener.BeginGetContext(OnRequest, null);
Trace("Options: " + options.ToString());
Trace($"Server started on port {options.Port}");
}

public void Stop()
{
lock (lockObj)
{
if (listener != null)
{
listener.Stop();
listener.Close();
listener = null;
}
}
Trace("Server stopped");
}

private void OnRequest(IAsyncResult ar)
{
Trace("OnRequest");
HttpListenerContext context;

lock (lockObj)
{
if (listener == null) return;
context = listener.EndGetContext(ar);
listener.BeginGetContext(OnRequest, null);
}

try
{
//Veriyi gönderirken eğer clienti kapatırsak hata alıyoruz. Bu yüzden try catch içerisine aldık.
HandleRequest(context);
}
catch (Exception ex)
{
Trace("Error on Handling Request: " + ex.Message);
}
}

private void SetHeader(HttpListenerContext context, byte[] content, bool isM3U8, bool isMp4)
{
/*
Accept-Ranges : bytes
Access-Control-Allow-Headers : origin, range
Access-Control-Allow-Methods : GET, HEAD, OPTIONS
Access-Control-Allow-Origin : *
Access-Control-Expose-Headers : Server,range
Content-Length : 14093
Content-Type : application/vnd.apple.mpegurl

Date: Mon, 03 Jun 2024 21:10:56 GMT
Etag: "usp-E130F796"
Last-Modified: Mon, 21 Jun 2021 14:30:27 GMT
Server: Apache/2.4.58 (Unix)
X-Usp: version=1.13.0 (29687)
*/
context.Response.AddHeader("Accept-Ranges", "bytes");
context.Response.AddHeader("Access-Control-Allow-Headers", "origin, range");
context.Response.AddHeader("Access-Control-Allow-Methods", "GET, HEAD, OPTIONS");
context.Response.AddHeader("Access-Control-Allow-Origin", "*");
context.Response.AddHeader("Access-Control-Expose-Headers", "Server,range");
//context.Response.AddHeader("Content-Length", content.Length.ToString());
context.Response.ContentLength64 = content.Length;

if (isM3U8)
{
context.Response.ContentType = "application/vnd.apple.mpegurl";
}
else
{
if (isMp4)
{
context.Response.ContentType = "video/mp4";
}
else
{
context.Response.ContentType = "video/MP2T";
}
}
}

private void HandleRequest(HttpListenerContext context)
{
string url = context.Request.Url!.AbsolutePath;
Trace($"Request for {url}");

if (url.EndsWith("/stream.m3u8"))
{
Trace("Sending M3U8 playlist");
string m3u8Content = GenerateM3U8Playlist();
byte[] buffer = Encoding.UTF8.GetBytes(m3u8Content);
SetHeader(context, buffer, true, false);
context.Response.OutputStream.Write(buffer, 0, buffer.Length);
Trace("M3U8 playlist sent");
}
else if (url.EndsWith("init.mp4"))
{
Trace("Sending init segment");
byte[] initSegment = GetInitSegment();
context.Response.ContentType = "video/mp4";
context.Response.ContentLength64 = initSegment.Length;
context.Response.AddHeader("Access-Control-Allow-Origin", "*"); // CORS için ekleme
context.Response.OutputStream.Write(initSegment, 0, initSegment.Length);
Trace("Init segment sent");
}
else if (url.EndsWith(".ts"))
{
Trace("Sending TS segment");
byte[] segment = GetNextSegment(options.SourceFPS * options.SegmentDuration);
context.Response.ContentType = "video/MP2T";
context.Response.ContentLength64 = segment.Length;
context.Response.AddHeader("Access-Control-Allow-Origin", "*"); // CORS için ekleme
context.Response.OutputStream.Write(segment, 0, segment.Length);
Trace("TS segment sent");
}
else if (url.EndsWith(".m4s"))
{
Trace("Sending M4S segment");
byte[] segment = GetNextSegment(options.SourceFPS * options.SegmentDuration);
context.Response.ContentType = "video/mp4";
context.Response.ContentLength64 = segment.Length;
context.Response.AddHeader("Access-Control-Allow-Origin", "*"); // CORS için ekleme
context.Response.OutputStream.Write(segment, 0, segment.Length);
Trace("M4S segment sent");
}
else if (options.LowLatencySupport && url.EndsWith(".part"))
{
Trace("Sending LL-HLS part");
byte[] part = GetNextPart((int)(options.SourceFPS * options.LLPartTarget));

if (options.SegmentRequestType == HLSSegmentRequestType.M4S)
{
context.Response.ContentType = "video/mp4";
}
else if (options.SegmentRequestType == HLSSegmentRequestType.TS)
{
context.Response.ContentType = "video/MP2T";
}

context.Response.ContentLength64 = part.Length;
context.Response.AddHeader("Access-Control-Allow-Origin", "*"); // CORS için ekleme
context.Response.OutputStream.Write(part, 0, part.Length);
partIndex++; // Increment part index for each part sent
Trace($"Part {partIndex} sent");
}
else
{
context.Response.StatusCode = (int)HttpStatusCode.NotFound;
}

context.Response.OutputStream.Close();
}

private string GetExt(HLSSegmentRequestType segmentRequestType)
{
return segmentRequestType == HLSSegmentRequestType.TS ? "ts" : "m4s";
}

string GenerateM3U8Playlist()
{
StringBuilder m3u8Builder = new StringBuilder();
m3u8Builder.AppendLine("#EXTM3U");

// Use version 7 for LL-HLS
m3u8Builder.AppendLine(options.LowLatencySupport ? "#EXT-X-VERSION:7" : "#EXT-X-VERSION:1");

m3u8Builder.AppendLine($"#EXT-X-MEDIA-SEQUENCE:{segmentIndex}");
m3u8Builder.AppendLine($"#EXT-X-TARGETDURATION:{options.SegmentDuration}");
//#USP-X-TIMESTAMP-MAP:MPEGTS=900000,LOCAL=1970-01-01T00:00:00Z
//m3u8Builder.AppendLine("#USP-X-TIMESTAMP-MAP:MPEGTS=900000,LOCAL=1970-01-01T00:00:00Z");

if (!options.LowLatencySupport)
{
/*
* #EXTINF:4, no desc
* segment-1.ts
* #EXTINF:4, no desc
* segment-2.ts
* #EXTINF:4, no desc
* segment-3.ts
*/

for (int i = 0; i < options.SegmentBufferCount; i++)
{
int segmentNumber = segmentIndex - (options.SegmentBufferCount - 1 - i);

if (segmentNumber >= 0)
{
m3u8Builder.AppendLine($"#EXTINF:{options.SegmentDuration}, no desc");
m3u8Builder.AppendLine($"segment{segmentNumber}.{GetExt(options.SegmentRequestType)}");
}
}
}

//if (options.LowLatencySupport)
//{
// m3u8Builder.AppendLine("#EXT-X-MAP:URI=\"init.mp4\"");
// m3u8Builder.AppendLine($"#EXT-X-PART-INF:PART-TARGET={options.LLPartTarget}");
// m3u8Builder.AppendLine($"#EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES,PART-HOLD-BACK={options.LLPartTarget * 3},HOLD-BACK={options.SegmentDuration * 3}");
// // Add parts for low latency
// for (int i = 0; i < options.SegmentBufferCount; i++)
// {
// int segmentNumber = segmentIndex - (options.SegmentBufferCount - 1 - i);
// if (segmentNumber >= 0)
// {
// for (int j = 0; j < (int)(options.SegmentDuration / options.LLPartTarget); j++)
// {
// m3u8Builder.AppendLine($"#EXT-X-PART:DURATION={options.LLPartTarget},INDEPENDENT=YES,URI=\"segment{segmentNumber}_part{j}{(options.SegmentRequestType == HLSSegmentRequestType.TS ? ".part" : ".m4s")}\"");
// }
// m3u8Builder.AppendLine($"#EXTINF:{options.SegmentDuration},");
// m3u8Builder.AppendLine($"segment{segmentNumber}{(options.SegmentRequestType == HLSSegmentRequestType.TS ? ".ts" : ".m4s")}");
// }
// }
//}
//else
//{
// m3u8Builder.AppendLine("#EXT-X-MAP:URI=\"init.mp4\"");
// // Add segments for standard HLS
// for (int i = 0; i < options.SegmentBufferCount; i++)
// {
// int segmentNumber = segmentIndex - (options.SegmentBufferCount - 1 - i);
// if (segmentNumber >= 0)
// {
// m3u8Builder.AppendLine($"#EXTINF:{options.SegmentDuration},");
// m3u8Builder.AppendLine($"segment{segmentNumber}{(options.SegmentRequestType == HLSSegmentRequestType.TS ? ".ts" : ".m4s")}");
// }
// }
//}

//listemiz sonsuz olacak şekilde ayarlanacağı için #EXT-X-ENDLIST eklemiyoruz.
lock (lockObj)
{
segmentIndex++;
}

return m3u8Builder.ToString();
}

public abstract byte[] GetNextSegment(int frames);
public abstract byte[] GetNextPart(int frames);
public abstract byte[] GetInitSegment(); // New method for init segment
}


Подробнее здесь: https://stackoverflow.com/questions/787 ... th-c-sharp
Ответить

Быстрый ответ

Изменение регистра текста: 
Смайлики
:) :( :oops: :roll: :wink: :muza: :clever: :sorry: :angel: :read: *x)
Ещё смайлики…
   
К этому ответу прикреплено по крайней мере одно вложение.

Если вы не хотите добавлять вложения, оставьте поля пустыми.

Максимально разрешённый размер вложения: 15 МБ.

Вернуться в «C#»