Created
October 15, 2019 19:16
-
-
Save lightfromshadows/79029ca480393270009173abc7cad858 to your computer and use it in GitHub Desktop.
Simple MJPEG stream decoder for Unity written in C#
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
/* | |
* I needed a simple MJPEG Stream Decoder and I couldn't find one that worked for me. | |
* | |
* It reads a response stream and when there's a new frame it updates the render texture. | |
* That's it. No authenication or options. | |
* It's something stupid simple for readimg a video stream from an equally stupid simple Arduino. | |
* | |
* I fixed most of the large memory leaks, but there's at least one small one left. | |
*/ | |
using System.IO; | |
using System.Threading; | |
using System.Collections; | |
using System.Collections.Generic; | |
using System.Net; | |
using UnityEngine; | |
public class MJPEGStreamDecoder : MonoBehaviour | |
{ | |
[SerializeField] bool tryOnStart = false; | |
[SerializeField] string defaultStreamURL = "http://127.0.0.1/stream"; | |
[SerializeField] RenderTexture renderTexture; | |
float RETRY_DELAY = 5f; | |
int MAX_RETRIES = 3; | |
int retryCount = 0; | |
byte[] nextFrame = null; | |
Thread worker; | |
int threadID = 0; | |
static System.Random randu; // I use my own System.Random instead of the shared UnityEngine.Random to avoid collisions | |
List<BufferedStream> trackedBuffers = new List<BufferedStream>(); | |
// Start is called before the first frame update | |
void Start() | |
{ | |
randu = new System.Random(Random.Range(0, 65536)); | |
if (tryOnStart) | |
StartStream(defaultStreamURL); | |
} | |
private void Update() | |
{ | |
if (nextFrame != null) | |
{ | |
SendFrame(nextFrame); | |
nextFrame = null; | |
} | |
} | |
private void OnDestroy() | |
{ | |
foreach (var b in trackedBuffers) | |
{ | |
if (b != null) | |
b.Close(); | |
} | |
} | |
public void StartStream(string url) | |
{ | |
retryCount = 0; | |
StopAllCoroutines(); | |
foreach (var b in trackedBuffers) | |
b.Close(); | |
worker = new Thread(() => ReadMJPEGStreamWorker(threadID = randu.Next(65536), url)); | |
worker.Start(); | |
} | |
void ReadMJPEGStreamWorker(int id, string url) | |
{ | |
var webRequest = WebRequest.Create(url); | |
webRequest.Method = "GET"; | |
List<byte> frameBuffer = new List<byte>(); | |
int lastByte = 0x00; | |
bool addToBuffer = false; | |
BufferedStream buffer = null; | |
try | |
{ | |
Stream stream = webRequest.GetResponse().GetResponseStream(); | |
buffer = new BufferedStream(stream); | |
trackedBuffers.Add(buffer); | |
} | |
catch (System.Exception ex) | |
{ | |
Debug.LogError(ex); | |
} | |
int newByte; | |
while (buffer != null) | |
{ | |
if (threadID != id) return; // We are no longer the active thread! stop doing things damnit! | |
if (!buffer.CanRead) | |
{ | |
Debug.LogError("Can't read buffer!"); | |
break; | |
} | |
newByte = -1; | |
try | |
{ | |
newByte = buffer.ReadByte(); | |
} | |
catch | |
{ | |
break; // Something happened to the stream, start a new one | |
} | |
if (newByte < 0) // end of stream or failure | |
{ | |
continue; // End of data | |
} | |
if (addToBuffer) | |
frameBuffer.Add((byte)newByte); | |
if (lastByte == 0xFF) // It's a command! | |
{ | |
if (!addToBuffer) // We're not reading a frame, should we be? | |
{ | |
if (IsStartOfImage(newByte)) | |
{ | |
addToBuffer = true; | |
frameBuffer.Add((byte)lastByte); | |
frameBuffer.Add((byte)newByte); | |
} | |
} | |
else // We're reading a frame, should we stop? | |
{ | |
if (newByte == 0xD9) | |
{ | |
frameBuffer.Add((byte)newByte); | |
addToBuffer = false; | |
nextFrame = frameBuffer.ToArray(); | |
frameBuffer.Clear(); | |
} | |
} | |
} | |
lastByte = newByte; | |
} | |
if (retryCount < MAX_RETRIES) | |
{ | |
retryCount++; | |
Debug.LogFormat("[{0}] Retrying Connection {1}...", id, retryCount); | |
foreach (var b in trackedBuffers) | |
b.Dispose(); | |
trackedBuffers.Clear(); | |
worker = new Thread(() => ReadMJPEGStreamWorker(threadID = randu.Next(65536), url)); | |
worker.Start(); | |
} | |
} | |
bool IsStartOfImage(int command) | |
{ | |
switch (command) | |
{ | |
case 0x8D: | |
Debug.Log("Command SOI"); | |
return true; | |
case 0xC0: | |
Debug.Log("Command SOF0"); | |
return true; | |
case 0xC2: | |
Debug.Log("Command SOF2"); | |
return true; | |
case 0xC4: | |
Debug.Log("Command DHT"); | |
break; | |
case 0xD8: | |
//Debug.Log("Command DQT"); | |
return true; | |
case 0xDD: | |
Debug.Log("Command DRI"); | |
break; | |
case 0xDA: | |
Debug.Log("Command SOS"); | |
break; | |
case 0xFE: | |
Debug.Log("Command COM"); | |
break; | |
case 0xD9: | |
Debug.Log("Command EOI"); | |
break; | |
} | |
return false; | |
} | |
void SendFrame(byte[] bytes) | |
{ | |
Texture2D texture2D = new Texture2D(2, 2); | |
texture2D.LoadImage(bytes); | |
//Debug.LogFormat("Loaded {0}b image [{1},{2}]", bytes.Length, texture2D.width, texture2D.height); | |
if (texture2D.width == 2) | |
return; // Failure! | |
Graphics.Blit(texture2D, renderTexture); | |
Destroy(texture2D); // LoadImage discards the previous buffer, so there's no point in trying to reuse it | |
} | |
} |
Hey how i can use it to stream a live stream in unity??
What steps should i take?
Its my first time using unity so I'm lost!
If you can help me I'd appreciate that.
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Thank you for sharing your implementation of MJPEG stream :D