Skip to content

Instantly share code, notes, and snippets.

@Larry57
Last active June 1, 2018 19:06
Show Gist options
  • Save Larry57/bd4b8e5d03a05db99a359617807bb59b to your computer and use it in GitHub Desktop.
Save Larry57/bd4b8e5d03a05db99a359617807bb59b to your computer and use it in GitHub Desktop.
*** Credits goes to [email protected] *** MJPEG stream decoder, adapted from AForge.NET so the NewFrame event gives an byte[] back instead of a Bitmap.
using System;
using System.IO;
using System.Net;
using System.Text;
using System.Threading;
// AForge Video Library
// AForge.NET framework
//
// Copyright © Andrew Kirillov, 2007-2008
// [email protected]
//
namespace Decoders {
class MJPEGStream {
// URL for MJPEG stream
private string source;
// received frames count
private int framesReceived;
// recieved byte count
private long bytesReceived;
// buffer size used to download MJPEG stream
private const int bufSize = 1024 * 1024;
// size of portion to read at once
private const int readSize = 1024;
private Thread thread = null;
private ManualResetEvent stopEvent = null;
private ManualResetEvent reloadEvent = null;
/// <summary>
/// New frame event.
/// </summary>
///
/// <remarks><para>Notifies clients about new available frame from video source.</para>
///
/// <para><note>Since video source may have multiple clients, each client is responsible for
/// making a copy (cloning) of the passed video frame, because the video source disposes its
/// own original copy after notifying of clients.</note></para>
/// </remarks>
///
public event Action<byte[]> NewFrame;
/// <summary>
/// Video source error event.
/// </summary>
///
/// <remarks>This event is used to notify clients about any type of errors occurred in
/// video source object, for example internal exceptions.</remarks>
///
public event Action<string> VideoSourceError;
/// <summary>
/// Video playing finished event.
/// </summary>
///
/// <remarks><para>This event is used to notify clients that the video playing has finished.</para>
/// </remarks>
///
public event Action<ReasonToFinishPlaying> PlayingFinished;
/// <summary>
/// Use or not separate connection group.
/// </summary>
///
/// <remarks>The property indicates to open web request in separate connection group.</remarks>
///
public bool SeparateConnectionGroup { get; set; } = true;
/// <summary>
/// Video source.
/// </summary>
///
/// <remarks>URL, which provides MJPEG stream.</remarks>
///
public string Source {
get { return source; }
set {
source = value;
// signal to reload
if(thread != null) {
reloadEvent.Set();
}
}
}
/// <summary>
/// Login value.
/// </summary>
///
/// <remarks>Login required to access video source.</remarks>
///
public string Login { get; set; } = null;
/// <summary>
/// Password value.
/// </summary>
///
/// <remarks>Password required to access video source.</remarks>
///
public string Password { get; set; } = null;
/// <summary>
/// Gets or sets proxy information for the request.
/// </summary>
///
/// <remarks><para>The local computer or application config file may specify that a default
/// proxy to be used. If the Proxy property is specified, then the proxy settings from the Proxy
/// property overridea the local computer or application config file and the instance will use
/// the proxy settings specified. If no proxy is specified in a config file
/// and the Proxy property is unspecified, the request uses the proxy settings
/// inherited from Internet Explorer on the local computer. If there are no proxy settings
/// in Internet Explorer, the request is sent directly to the server.
/// </para></remarks>
///
public IWebProxy Proxy { get; set; } = null;
/// <summary>
/// User agent to specify in HTTP request header.
/// </summary>
///
/// <remarks><para>Some IP cameras check what is the requesting user agent and depending
/// on it they provide video in different formats or do not provide it at all. The property
/// sets the value of user agent string, which is sent to camera in request header.
/// </para>
///
/// <para>Default value is set to "Mozilla/5.0". If the value is set to <see langword="null"/>,
/// the user agent string is not sent in request header.</para>
/// </remarks>
///
public string HttpUserAgent { get; set; } = "Mozilla/5.0";
/// <summary>
/// Received frames count.
/// </summary>
///
/// <remarks>Number of frames the video source provided from the moment of the last
/// access to the property.
/// </remarks>
///
public int FramesReceived {
get {
int frames = framesReceived;
framesReceived = 0;
return frames;
}
}
/// <summary>
/// Received bytes count.
/// </summary>
///
/// <remarks>Number of bytes the video source provided from the moment of the last
/// access to the property.
/// </remarks>
///
public long BytesReceived {
get {
long bytes = bytesReceived;
bytesReceived = 0;
return bytes;
}
}
/// <summary>
/// Request timeout value.
/// </summary>
///
/// <remarks>The property sets timeout value in milliseconds for web requests.
/// Default value is 10000 milliseconds.</remarks>
///
public int RequestTimeout { get; set; } = 10000;
/// <summary>
/// State of the video source.
/// </summary>
///
/// <remarks>Current state of video source object - running or not.</remarks>
///
public bool IsRunning {
get {
if(thread != null) {
// check thread status
if(thread.Join(0) == false) {
return true;
}
// the thread is not running, so free resources
Free();
}
return false;
}
}
/// <summary>
/// Force using of basic authentication when connecting to the video source.
/// </summary>
///
/// <remarks><para>For some IP cameras (TrendNET IP cameras, for example) using standard .NET's authentication via credentials
/// does not seem to be working (seems like camera does not request for authentication, but expects corresponding headers to be
/// present on connection request). So this property allows to force basic authentication by adding required HTTP headers when
/// request is sent.</para>
///
/// <para>Default value is set to <see langword="false"/>.</para>
/// </remarks>
///
public bool ForceBasicAuthentication { get; set; }
/// <summary>
/// Initializes a new instance of the <see cref="MJPEGStream"/> class.
/// </summary>
///
public MJPEGStream() { }
/// <summary>
/// Initializes a new instance of the <see cref="MJPEGStream"/> class.
/// </summary>
///
/// <param name="source">URL, which provides MJPEG stream.</param>
///
public MJPEGStream(string source) {
this.source = source;
}
/// <summary>
/// Start video source.
/// </summary>
///
/// <remarks>Starts video source and return execution to caller. Video source
/// object creates background thread and notifies about new frames with the
/// help of <see cref="NewFrame"/> event.</remarks>
///
/// <exception cref="ArgumentException">Video source is not specified.</exception>
///
public void Start() {
if(!IsRunning) {
// check source
if((source == null) || (source == string.Empty)) {
throw new ArgumentException("Video source is not specified.");
}
framesReceived = 0;
bytesReceived = 0;
// create events
stopEvent = new ManualResetEvent(false);
reloadEvent = new ManualResetEvent(false);
// create and start new thread
thread = new Thread(new ThreadStart(WorkerThread)) {
Name = source
};
thread.Start();
}
}
/// <summary>
/// Signal video source to stop its work.
/// </summary>
///
/// <remarks>Signals video source to stop its background thread, stop to
/// provide new frames and free resources.</remarks>
///
public void SignalToStop() {
// stop thread
if(thread != null) {
// signal to stop
stopEvent.Set();
}
}
/// <summary>
/// Wait for video source has stopped.
/// </summary>
///
/// <remarks>Waits for source stopping after it was signalled to stop using
/// <see cref="SignalToStop"/> method.</remarks>
///
public void WaitForStop() {
if(thread != null) {
// wait for thread stop
thread.Join();
Free();
}
}
/// <summary>
/// Stop video source.
/// </summary>
///
/// <remarks><para>Stops video source aborting its thread.</para>
///
/// <para><note>Since the method aborts background thread, its usage is highly not preferred
/// and should be done only if there are no other options. The correct way of stopping camera
/// is <see cref="SignalToStop">signaling it stop</see> and then
/// <see cref="WaitForStop">waiting</see> for background thread's completion.</note></para>
/// </remarks>
///
public void Stop() {
if(this.IsRunning) {
stopEvent.Set();
thread.Abort();
WaitForStop();
}
}
/// <summary>
/// Free resource.
/// </summary>
///
private void Free() {
thread = null;
// release events
stopEvent.Close();
stopEvent = null;
reloadEvent.Close();
reloadEvent = null;
}
// Worker thread
private void WorkerThread() {
// buffer to read stream
byte[] buffer = new byte[bufSize];
// JPEG magic number
byte[] jpegMagic = new byte[] { 0xFF, 0xD8, 0xFF };
int jpegMagicLength = 3;
ASCIIEncoding encoding = new ASCIIEncoding();
while(!stopEvent.WaitOne(0, false)) {
// reset reload event
reloadEvent.Reset();
// HTTP web request
HttpWebRequest request = null;
// web responce
WebResponse response = null;
// stream for MJPEG downloading
Stream stream = null;
// boundary betweeen images (string and binary versions)
byte[] boundary = null;
string boudaryStr = null;
// length of boundary
int boundaryLen;
// flag signaling if boundary was checked or not
bool boundaryIsChecked = false;
// read amounts and positions
int read, todo = 0, total = 0, pos = 0, align = 1;
int start = 0, stop = 0;
// align
// 1 = searching for image start
// 2 = searching for image end
try {
// create request
request = (HttpWebRequest)WebRequest.Create(source);
// set user agent
if(HttpUserAgent != null) {
request.UserAgent = HttpUserAgent;
}
// set proxy
if(Proxy != null) {
request.Proxy = Proxy;
}
// set timeout value for the request
request.Timeout = RequestTimeout;
// set login and password
if((Login != null) && (Password != null) && (Login != string.Empty)) {
request.Credentials = new NetworkCredential(Login, Password);
}
// set connection group name
if(SeparateConnectionGroup) {
request.ConnectionGroupName = GetHashCode().ToString();
}
// force basic authentication through extra headers if required
if(ForceBasicAuthentication) {
string authInfo = string.Format("{0}:{1}", Login, Password);
authInfo = Convert.ToBase64String(Encoding.Default.GetBytes(authInfo));
request.Headers["Authorization"] = "Basic " + authInfo;
}
// get response
response = request.GetResponse();
// check content type
string contentType = response.ContentType;
string[] contentTypeArray = contentType.Split('/');
// "application/octet-stream"
if((contentTypeArray[0] == "application") && (contentTypeArray[1] == "octet-stream")) {
boundaryLen = 0;
boundary = new byte[0];
}
else if((contentTypeArray[0] == "multipart") && (contentType.Contains("mixed"))) {
// get boundary
int boundaryIndex = contentType.IndexOf("boundary", 0);
if(boundaryIndex != -1) {
boundaryIndex = contentType.IndexOf("=", boundaryIndex + 8);
}
if(boundaryIndex == -1) {
// try same scenario as with octet-stream, i.e. without boundaries
boundaryLen = 0;
boundary = new byte[0];
}
else {
boudaryStr = contentType.Substring(boundaryIndex + 1);
// remove spaces and double quotes, which may be added by some IP cameras
boudaryStr = boudaryStr.Trim(' ', '"');
boundary = encoding.GetBytes(boudaryStr);
boundaryLen = boundary.Length;
boundaryIsChecked = false;
}
}
else {
throw new Exception("Invalid content type.");
}
// get response stream
stream = response.GetResponseStream();
stream.ReadTimeout = RequestTimeout;
// loop
while((!stopEvent.WaitOne(0, false)) && (!reloadEvent.WaitOne(0, false))) {
// check total read
if(total > bufSize - readSize) {
total = pos = todo = 0;
}
// read next portion from stream
if((read = stream.Read(buffer, total, readSize)) == 0) {
throw new ApplicationException();
}
total += read;
todo += read;
// increment received bytes counter
bytesReceived += read;
// do we need to check boundary ?
if((boundaryLen != 0) && (!boundaryIsChecked)) {
// some IP cameras, like AirLink, claim that boundary is "myboundary",
// when it is really "--myboundary". this needs to be corrected.
pos = ByteArrayUtils.Find(buffer, boundary, 0, todo);
// continue reading if boudary was not found
if(pos == -1) {
continue;
}
for(int i = pos - 1; i >= 0; i--) {
byte ch = buffer[i];
if((ch == (byte)'\n') || (ch == (byte)'\r')) {
break;
}
boudaryStr = (char)ch + boudaryStr;
}
boundary = encoding.GetBytes(boudaryStr);
boundaryLen = boundary.Length;
boundaryIsChecked = true;
}
// search for image start
if((align == 1) && (todo >= jpegMagicLength)) {
start = ByteArrayUtils.Find(buffer, jpegMagic, pos, todo);
if(start != -1) {
// found JPEG start
pos = start + jpegMagicLength;
todo = total - pos;
align = 2;
}
else {
// delimiter not found
todo = jpegMagicLength - 1;
pos = total - todo;
}
}
// search for image end ( boundaryLen can be 0, so need extra check )
while((align == 2) && (todo != 0) && (todo >= boundaryLen)) {
stop = ByteArrayUtils.Find(buffer,
(boundaryLen != 0) ? boundary : jpegMagic,
pos, todo);
if(stop != -1) {
pos = stop;
todo = total - pos;
// increment frames counter
framesReceived++;
// image at stop
if((NewFrame != null) && (!stopEvent.WaitOne(0, false))) {
var frame = new byte[stop - start];
Array.Copy(buffer, start, frame, 0, stop - start);
NewFrame(frame);
}
// shift array
pos = stop + boundaryLen;
todo = total - pos;
Array.Copy(buffer, pos, buffer, 0, todo);
total = todo;
pos = 0;
align = 1;
}
else {
// boundary not found
if(boundaryLen != 0) {
todo = boundaryLen - 1;
pos = total - todo;
}
else {
todo = 0;
pos = total;
}
}
}
}
}
catch(ApplicationException) {
// do nothing for Application Exception, which we raised on our own
// wait for a while before the next try
Thread.Sleep(250);
}
catch(ThreadAbortException) {
break;
}
catch(Exception exception) {
// provide information to clients
VideoSourceError?.Invoke(exception.Message);
// wait for a while before the next try
Thread.Sleep(250);
}
finally {
// abort request
if(request != null) {
request.Abort();
request = null;
}
// close response stream
if(stream != null) {
stream.Close();
stream = null;
}
// close response
if(response != null) {
response.Close();
response = null;
}
}
// need to stop ?
if(stopEvent.WaitOne(0, false)) {
break;
}
}
PlayingFinished?.Invoke(ReasonToFinishPlaying.StoppedByUser);
}
static class ByteArrayUtils {
/// <summary>
/// Check if the array contains needle at specified position.
/// </summary>
///
/// <param name="array">Source array to check for needle.</param>
/// <param name="needle">Needle we are searching for.</param>
/// <param name="startIndex">Start index in source array.</param>
///
/// <returns>Returns <b>true</b> if the source array contains the needle at
/// the specified index. Otherwise it returns <b>false</b>.</returns>
///
public static bool Compare(byte[] array, byte[] needle, int startIndex) {
int needleLen = needle.Length;
// compare
for(int i = 0, p = startIndex; i < needleLen; i++, p++) {
if(array[p] != needle[i]) {
return false;
}
}
return true;
}
/// <summary>
/// Find subarray in the source array.
/// </summary>
///
/// <param name="array">Source array to search for needle.</param>
/// <param name="needle">Needle we are searching for.</param>
/// <param name="startIndex">Start index in source array.</param>
/// <param name="sourceLength">Number of bytes in source array, where the needle is searched for.</param>
///
/// <returns>Returns starting position of the needle if it was found or <b>-1</b> otherwise.</returns>
///
public static int Find(byte[] array, byte[] needle, int startIndex, int sourceLength) {
int needleLen = needle.Length;
int index;
while(sourceLength >= needleLen) {
// find needle's starting element
index = Array.IndexOf(array, needle[0], startIndex, sourceLength - needleLen + 1);
// if we did not find even the first element of the needls, then the search is failed
if(index == -1) {
return -1;
}
int i, p;
// check for needle
for(i = 0, p = index; i < needleLen; i++, p++) {
if(array[p] != needle[i]) {
break;
}
}
if(i == needleLen) {
// needle was found
return index;
}
// continue to search for needle
sourceLength -= (index - startIndex + 1);
startIndex = index + 1;
}
return -1;
}
}
}
/// <summary>
/// Reason of finishing video playing.
/// </summary>
///
/// <remarks><para>When video source class fire the <see cref="IVideoSource.PlayingFinished"/> event, they
/// need to specify reason of finishing video playing. For example, it may be end of stream reached.</para></remarks>
///
public enum ReasonToFinishPlaying {
/// <summary>
/// Video playing has finished because it end was reached.
/// </summary>
EndOfStreamReached,
/// <summary>
/// Video playing has finished because it was stopped by user.
/// </summary>
StoppedByUser,
/// <summary>
/// Video playing has finished because the device was lost (unplugged).
/// </summary>
DeviceLost,
/// <summary>
/// Video playing has finished because of some error happened the video source (camera, stream, file, etc.).
/// A error reporting event usually is fired to provide error information.
/// </summary>
VideoSourceError
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment