Last active
June 1, 2018 19:06
-
-
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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