0

I want to play video replay from low-end surveillance camera. Replays are saved on the camera in .mp4 format, with moov atom at the end. It's possible to retrieve file via http request using digset authentication. Approximate size of each video file is 20 MB, but download speed is only 3 Mbps, so downloading whole file takes about 60 s. This is to long, so I want to start displaying video before whole file will be downloaded.

Web browsers handles this kind of problem by reading end of file at the begining. I want to achieve same goal using c# and libvlcsharp, so created HttpMediaInput class.

public class HttpMediaInput : MediaInput
    {
        private static readonly NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();

        private HttpClientHandler _handler;
        private HttpClient _httpClient;
        private string _url;
        Stream _stream = null;

        public HttpMediaInput(string url, string username, string password)
        {
            _url = url;
            _handler = new HttpClientHandler() { Credentials = new NetworkCredential(username, password) };
            _httpClient = new HttpClient(_handler);
        }

        public override bool Open(out ulong size)
        {
            size = ulong.MaxValue;
            try
            {
                _stream = _httpClient.GetStreamAsync(_url).Result;
                base.CanSeek = _stream.CanSeek;
                return true;
            }
            catch (Exception ex)
            {
                logger.Error(ex, $"Exception occurred during sending stream request to url: {_url}");
                return false;
            }
        }

        public unsafe override int Read(IntPtr buf, uint len)
        {
            try
            {
                byte[] buffer = new byte[len];
                int bytesReaded = _stream.Read(buffer, 0, buffer.Length);
                logger.Trace($"Bytes readed: {bytesReaded}");
                Span<byte> byteSpan = new Span<byte>(buf.ToPointer(), buffer.Length);
                buffer.CopyTo(byteSpan);
                
                return bytesReaded;
            }
            catch (Exception ex)
            {
                logger.Error(ex, "Stream read exception");
                return -1;
            }
        }

       ...
        
    }

It works great for mp4 files that have all necessary metadata stored on the beginning, but no video is displayed in case of my camera.

Assuming that I will be able to download moov atom from mp4 using http range requests, how to provide this data to libvlc? Is it even possible?

I'm developing application using C#, WPF, dotnet framework.

ElPato
  • 11
  • 4
  • 1
    Is your Http stream you get from the http client actually seekable ? Why are you not using libvlc to play the http content? I think it does support digest authentication. Does it work with the VLC app itself ? – cube45 Jul 15 '22 at 21:47
  • Stream is not seekable. I get 401 errors (unauthorized) when I'm trying play video via VLC or using uri via libvlcsharp. I sniffed communication between VLC and camera. VLC is providing security header, but for some reason it's using [basic](https://datatracker.ietf.org/doc/html/rfc7617) authentication scheme instead of [digest](https://datatracker.ietf.org/doc/html/rfc7616). – ElPato Jul 18 '22 at 13:00
  • If your stream is not seekable, VLC can't go to the end of the file. You must find a way to make it seekable, like implementing a range http request. As for VLC not being able to perform digest auth, please file a bug to the gitlab repo – cube45 Jul 18 '22 at 17:41
  • @ElPato, So you cannot range-request any byte(s) of the MP4 file? At such point, you're gonna need professional help. The short version is that you'll have to dynamically create an MP4 header and attach the `mdat` section to it (since you will receive those bytes first). You'll have to also save one previous video for bytes study n order to "structure" your MP4 header (use it as template). Check bytes by using a hex editor. See if possible to repackage MP4 data as Frag-MP4 or even FLV or AVI... Secondly VLC does not accept bytes for playback. Try FFplay or try the Web Browser component. – VC.One Jul 22 '22 at 17:33
  • @ElPato PS: Better if I had said... _"You'll have to dynamically create a **fragmented** MP4 header (by code) and then attach the mdat section to it"_ After that the bytes can be played in a browser component (of C#) or any media player system that accepts MP4 bytes. I don't know if your problem is solved already but that's your best option, if you cannot just read the ending bytes to get the original MP4 header. Most servers can seek to any point in file and return those bytes, so there must be a setting you missed somewhere. Good luck. – VC.One Jul 25 '22 at 13:53

1 Answers1

1

VLC cannot play files from camera because http digest auth with md5 is considered to be deprecated (related issue in VLC repo).

However, I was able to resolve this problem following cube45 suggestions, I implemented range requests.

    public override bool Open(out ulong size)
        {
            size = ulong.MaxValue;
            try
            {
                HttpRequestMessage requestMessage = new HttpRequestMessage { RequestUri = new Uri(_url) };
                requestMessage.Headers.Range = new System.Net.Http.Headers.RangeHeaderValue();
                requestMessage.Method = HttpMethod.Head;
                var response = _httpClient.SendAsync(requestMessage).Result;
                size = (ulong)response.Content.Headers.ContentLength;
                _fileSize = size;
                logger.Trace($"Received content lenght | {size}");
                base.CanSeek = true;
                return true;
            }
            catch (Exception ex)
            {
                logger.Error(ex, $"Exception occurred during sending head request to url: {_url}");
                return false;
            }
        }

        public unsafe override int Read(IntPtr buf, uint len)
        {
            try
            {
                HttpRequestMessage requestMessage = new HttpRequestMessage { RequestUri = new Uri(_url) };
                long startReadPosition = (long)_currentPosition;
                long stopReadPosition = (long)_currentPosition + ((long)_numberOfBytesToReadInOneRequest - 1);
                if ((ulong)stopReadPosition > _fileSize)
                {
                    stopReadPosition = (long)_fileSize;
                }
                requestMessage.Headers.Range = new System.Net.Http.Headers.RangeHeaderValue(startReadPosition, stopReadPosition);
                requestMessage.Method = HttpMethod.Get;
                HttpResponseMessage response = _httpClient.SendAsync(requestMessage).Result;
                byte[] readedBytes = response.Content.ReadAsByteArrayAsync().Result;
                int readedBytesCount = readedBytes.Length;
                _currentPosition += (ulong)readedBytesCount;
                logger.Trace($"Bytes readed | {readedBytesCount} | startReadPosition {startReadPosition} | stopReadPosition | {stopReadPosition}");
                Span<byte> byteSpan = new Span<byte>(buf.ToPointer(), (int)len);
                readedBytes.CopyTo(byteSpan);

                return readedBytesCount;
            }
            catch (Exception ex)
            {
                logger.Error(ex, "Media reading general exception");
                return -1;
            }
        }

        public override bool Seek(ulong offset)
        {
            try
            {
                logger.Trace($"Seeking media with offset | {offset}");
                _currentPosition = offset;
                return true;
            }
            catch (Exception ex)
            {
                logger.Error(ex, "MediaInput seekeing general error");
                return false;
            }
        }

This solution seams to work, but there are two unresolved problems:

  1. There is about 8s lag between libvlcsharp starts reading stream and video goes live (waiting time in web browser is about 2s).
  2. Some part of video file at the end is not displayed, because the buffer is too short to hold whole file inside. Related thread
ElPato
  • 11
  • 4