Прямая трансляция FLV в С# WebApi

В настоящее время я работаю в прямом эфире с помощью webapi. Получая поток flv непосредственно из ffmpeg и отправляя его прямо клиенту с помощью PushStreamContent. Это отлично работает, если веб-страница уже открыта при запуске потока. Проблема в том, что когда я открываю другую страницу или обновляю эту страницу, вы больше не можете просматривать поток (поток по-прежнему отправляется клиенту в порядке). Я думаю, что это из-за чего-то не хватает с самого начала потока, но я не уверен, что делать. Любые указатели будут очень благодарны.

Код для потока чтения клиента

public class VideosController : ApiController
{
    public HttpResponseMessage Get()
    {
        var response = Request.CreateResponse();
        response.Content = new PushStreamContent(WriteToStream, new MediaTypeHeaderValue("video/x-flv"));

        return response;
    }

    private async Task WriteToStream( Stream arg1, HttpContent arg2, TransportContext arg3 )
    {
        //I think metadata needs to be written here but not sure how
        Startup.AddSubscriber( arg1 );
        await Task.Yield();
    }
}

Код для приема потока и последующей отправки клиенту

while (true)
{
    bytes = new byte[8024000];
    int bytesRec = handler.Receive(bytes);

    foreach (var subscriber in Startup.Subscribers.ToList())
    {
        var theSubscriber = subscriber;
        try
        {
            await theSubscriber.WriteAsync( bytes, 0, bytesRec );
        }
        catch
        {
            Startup.Subscribers.Remove(theSubscriber);
        }
    }
}

Ответы

Ответ 1

Я никогда не использовал FLV или не изучал видеоформаты.

Большинство форматов файлов структурированы, особенно видеоформаты. Они содержат кадры (т.е. Полный или частичный скриншоты в зависимости от формата сжатия).

Вам действительно должно быть повезло, если вам удастся попасть в конкретный кадр при запуске потоковой передачи новому подписчику. Следовательно, когда они начинают получать поток, они не могут идентифицировать формат, поскольку кадр является частичным.

Вы можете прочитать больше кадров FLV в статье в википедии. Это, скорее всего, ваша проблема.

Простая попытка состояла бы в том, чтобы попытаться сохранить исходный заголовок, который вы получаете с потокового сервера, когда подключается первый абонент.

Что-то вроде:

static byte _header = new byte[9]; //signature, version, flags, headerSize

public void YourStreamMethod()
{
    int bytesRec = handler.Receive(bytes);
    if (!_headerIsStored)
    {
        //store header
        Buffer.BlockCopy(bytes, 0, _header, 0, 9);
        _headerIsStored = true;
    }
}

.., который позволяет отправить заголовок следующему подключающемуся абоненту:

private async Task WriteToStream( Stream arg1, HttpContent arg2, TransportContext arg3 )
{
    // send the FLV header
    arg1.Write(_header, 0, 9);

    Startup.AddSubscriber( arg1 );
    await Task.Yield();
}

Сделав это, молись, чтобы приемник проигнорировал частичные кадры. Если это не так, вам нужно проанализировать поток, чтобы определить, где находится следующий кадр.

Для этого вам нужно сделать что-то вроде этого:

  • Создайте переменную BytesLeftToNextFrame.
  • Сохранить полученный пакетный заголовок в буфере
  • Преобразовать биты размера полезной нагрузки в int
  • Reset BytesLeftToNextFrame к анализируемому значению
  • Обратный отсчет до следующего раза, когда вы должны прочитать заголовок.

Наконец, когда новый клиент подключается, не запускайте потоковое вещание, пока не узнаете, что приходит следующий кадр.

Псевдокод:

var bytesLeftToNextFrame = 0;
while (true)
{
    bytes = new byte[8024000];
    int bytesRec = handler.Receive(bytes);

    foreach (var subscriber in Startup.Subscribers.ToList())
    {
        var theSubscriber = subscriber;
        try
        {
            if (subscriber.IsNew && bytesLeftToNextFrame < bytesRec)
            {
                //start from the index where the new frame starts
                await theSubscriber.WriteAsync( bytes, bytesLeftToNextFrame, bytesRec - bytesLeftToNextFrame);
                subscriber.IsNew = false;
            }
            else
            {
                //send everything, since we've already in streaming mode
                await theSubscriber.WriteAsync( bytes, 0, bytesRec );
            }
        }
        catch
        {
            Startup.Subscribers.Remove(theSubscriber);
        }
    }

    //TODO: check if the current frame is done
    // then parse the next header and reset the counter.
}

Ответ 2

Я не эксперт в потоковой передаче, но похоже, что вы должны закрыть поток, тогда все данные будут называться

await theSubscriber.WriteAsync( bytes, 0, bytesRec );

Как упоминается в WebAPI StreamContent vs PushStreamContent

{
     // After save we close the stream to signal that we are done writing.
     xDoc.Save(stream);
     stream.Close();
}

Ответ 3

Мне нравится этот код, так как он обрабатывает ОСНОВНУЮ ОШИБКУ при работе с асинхронным программированием

while (true)
{

}

это синхронизированный цикл, который как можно быстрее выполняет петли... каждую секунду он может выполняться тысячи раз (в зависимости от доступных программных и аппаратных ресурсов)

await theSubscriber.WriteAsync( bytes, 0, bytesRec );

это команда async (если это было недостаточно ясно), которые выполняются в DIFFERENT потоке (while цикл представляет выполнение основного потока)

теперь..., чтобы заставить цикл while ждать команды async, мы используем await... звучит хорошо (или цикл while будет выполняться тысячи раз, выполняя бесчисленные асинхронные команд)

НО, потому что цикл (подписчиков) должен передавать поток для всех подписчиков simulatanly, он зацикливается на ключевом слове ожидания

ЧТО ПОЧЕМУ РЕЗЬБА/НОВЫЙ ПОДПИСЧИК ЗАМОРАЖИВАТЬ ВЕСЬ (новое соединение = новый подписчик)

заключение: весь цикл for должен находиться внутри задачи. Задача должна ждать, пока сервер отправит поток всем подписчикам. ТОЛЬКО ТОГО, что он должен продолжать цикл while с помощью ContinueWith (поэтому он так назвал, верно?)

поэтому... команде write нужно выполнить выполнение без ожидания ключевого слова

theSubscriber.WriteAsync

цикл foreach должен использовать задачу, которая продолжается с циклом while после его завершения.