diff --git a/CMakeLists.txt b/CMakeLists.txt index 7cfb5be..22465de 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -310,6 +310,7 @@ if(NOT NO_EXAMPLES AND NOT CMAKE_SYSTEM_NAME STREQUAL "WindowsStore") add_subdirectory(examples/client) add_subdirectory(examples/media) add_subdirectory(examples/sfu-media) + add_subdirectory(examples/streamer) add_subdirectory(examples/copy-paste) add_subdirectory(examples/copy-paste-capi) endif() diff --git a/examples/streamer/CMakeLists.txt b/examples/streamer/CMakeLists.txt new file mode 100644 index 0000000..261f216 --- /dev/null +++ b/examples/streamer/CMakeLists.txt @@ -0,0 +1,22 @@ +cmake_minimum_required(VERSION 3.7) +if(POLICY CMP0079) + cmake_policy(SET CMP0079 NEW) +endif() + +if(WIN32) +add_executable(streamer main.cpp dispatchqueue.cpp dispatchqueue.hpp h264fileparser.cpp h264fileparser.hpp helpers.cpp helpers.hpp opusfileparser.cpp opusfileparser.hpp fileparser.cpp fileparser.hpp stream.cpp stream.hpp) +target_compile_definitions(streamer PUBLIC STATIC_GETOPT) +else() +add_executable(streamer main.cpp dispatchqueue.cpp dispatchqueue.hpp h264fileparser.cpp h264fileparser.hpp helpers.cpp helpers.hpp opusfileparser.cpp opusfileparser.hpp fileparser.cpp fileparser.hpp stream.cpp stream.hpp) +endif() +set_target_properties(streamer PROPERTIES + CXX_STANDARD 17 + OUTPUT_NAME streamer) + +if(WIN32) + target_link_libraries(streamer datachannel-static) # DLL exports only the C API +else() + target_link_libraries(streamer datachannel) +endif() +target_link_libraries(streamer datachannel nlohmann_json) + diff --git a/examples/streamer/README.md b/examples/streamer/README.md new file mode 100644 index 0000000..7301434 --- /dev/null +++ b/examples/streamer/README.md @@ -0,0 +1,32 @@ +# Streaming H264 and opus + +This example streams H264 and opus[1](#f1) samples to the connected browser client. + +## Starting signaling server + +```sh +$ python3 ../signaling-server-python/signaling-server.py +``` + +## Starting php + +```sh +$ php -S 127.0.0.1:8080 +``` + +Now you can open demo at [127.0.0.1:8080](127.0.0.1:8080). + +## Arguments + +- `-a` Directory with OPUS samples (default: *../../../../examples/streamer/samples/opus/*). +- `-b` Directory with H264 samples (default: *../../../../examples/streamer/samples/h264/*). +- `-d` Signaling server IP address (default: 127.0.0.1). +- `-p` Signaling server port (default: 8000). +- `-v` Enable debug logs. +- `-h` Print this help and exit. + +## Generating H264 and Opus samples + +You can generate H264 and Opus sample with *samples/generate_h264.py* and *samples/generate_opus.py* respectively. This require ffmpeg, python3 and kaitaistruct library to be installed. Use `-h`/`--help` to learn more about arguments. + +1 Opus samples are generated from music downloaded at [bensound](https://www.bensound.com). [↩](#a1) diff --git a/examples/streamer/client.js b/examples/streamer/client.js new file mode 100644 index 0000000..6d040cd --- /dev/null +++ b/examples/streamer/client.js @@ -0,0 +1,209 @@ +/** @type {RTCPeerConnection} */ +let rtc; +const iceConnectionLog = document.getElementById('ice-connection-state'), + iceGatheringLog = document.getElementById('ice-gathering-state'), + signalingLog = document.getElementById('signaling-state'), + dataChannelLog = document.getElementById('data-channel'); + +function randomString(len) { + const charSet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; + let randomString = ''; + for (let i = 0; i < len; i++) { + const randomPoz = Math.floor(Math.random() * charSet.length); + randomString += charSet.substring(randomPoz, randomPoz + 1); + } + return randomString; +} + +const receiveID = randomString(10); +const websocket = new WebSocket('ws://127.0.0.1:8000/' + receiveID); +websocket.onopen = function () { + document.getElementById('start').disabled = false; +} + +// data channel +let dc = null, dcTimeout = null; + +function createPeerConnection() { + const config = { + sdpSemantics: 'unified-plan', + bundlePolicy: "max-bundle", + }; + + if (document.getElementById('use-stun').checked) { + config.iceServers = [{urls: ['stun:stun.l.google.com:19302']}]; + } + + let pc = new RTCPeerConnection(config); + + // register some listeners to help debugging + pc.addEventListener('icegatheringstatechange', function () { + iceGatheringLog.textContent += ' -> ' + pc.iceGatheringState; + }, false); + iceGatheringLog.textContent = pc.iceGatheringState; + + pc.addEventListener('iceconnectionstatechange', function () { + iceConnectionLog.textContent += ' -> ' + pc.iceConnectionState; + }, false); + iceConnectionLog.textContent = pc.iceConnectionState; + + pc.addEventListener('signalingstatechange', function () { + signalingLog.textContent += ' -> ' + pc.signalingState; + }, false); + signalingLog.textContent = pc.signalingState; + + // connect audio / video + pc.addEventListener('track', function (evt) { + if (evt.track.kind == 'video') { + document.getElementById('media').style.display = 'block'; + document.getElementById('video').srcObject = evt.streams[0]; + } else { + document.getElementById('audio').srcObject = evt.streams[0]; + } + }); + + let time_start = null; + + function current_stamp() { + if (time_start === null) { + time_start = new Date().getTime(); + return 0; + } else { + return new Date().getTime() - time_start; + } + } + + pc.ondatachannel = function (event) { + dc = event.channel; + dc.onopen = function () { + dataChannelLog.textContent += '- open\n'; + dataChannelLog.scrollTop = dataChannelLog.scrollHeight; + }; + dc.onmessage = function (evt) { + + dataChannelLog.textContent += '< ' + evt.data + '\n'; + dataChannelLog.scrollTop = dataChannelLog.scrollHeight; + + dcTimeout = setTimeout(function () { + if (dc == null && dcTimeout != null) { + dcTimeout = null; + return + } + const message = 'Pong ' + current_stamp(); + dataChannelLog.textContent += '> ' + message + '\n'; + dataChannelLog.scrollTop = dataChannelLog.scrollHeight; + dc.send(message); + }, 1000); + } + dc.onclose = function () { + clearTimeout(dcTimeout); + dcTimeout = null; + dataChannelLog.textContent += '- close\n'; + dataChannelLog.scrollTop = dataChannelLog.scrollHeight; + }; + } + + return pc; +} + +function sendAnswer(pc) { + return pc.createAnswer() + .then((answer) => rtc.setLocalDescription(answer)) + .then(function () { + // wait for ICE gathering to complete + return new Promise(function (resolve) { + if (pc.iceGatheringState === 'complete') { + resolve(); + } else { + function checkState() { + if (pc.iceGatheringState === 'complete') { + pc.removeEventListener('icegatheringstatechange', checkState); + resolve(); + } + } + + pc.addEventListener('icegatheringstatechange', checkState); + } + }); + }).then(function () { + const answer = pc.localDescription; + + document.getElementById('answer-sdp').textContent = answer.sdp; + + return websocket.send(JSON.stringify( + { + id: "server", + type: answer.type, + sdp: answer.sdp, + })); + }).catch(function (e) { + alert(e); + }); +} + +function handleOffer(offer) { + rtc = createPeerConnection(); + return rtc.setRemoteDescription(offer) + .then(() => sendAnswer(rtc)); +} + +function sendStreamRequest() { + websocket.send(JSON.stringify( + { + id: "server", + type: "streamRequest", + receiver: receiveID, + })); +} + +async function start() { + document.getElementById('start').style.display = 'none'; + document.getElementById('stop').style.display = 'inline-block'; + document.getElementById('media').style.display = 'block'; + sendStreamRequest(); +} + +function stop() { + document.getElementById('stop').style.display = 'none'; + document.getElementById('media').style.display = 'none'; + document.getElementById('start').style.display = 'inline-block'; + + // close data channel + if (dc) { + dc.close(); + dc = null; + } + + // close transceivers + if (rtc.getTransceivers) { + rtc.getTransceivers().forEach(function (transceiver) { + if (transceiver.stop) { + transceiver.stop(); + } + }); + } + + // close local audio / video + rtc.getSenders().forEach(function (sender) { + const track = sender.track; + if (track !== null) { + sender.track.stop(); + } + }); + + // close peer connection + setTimeout(function () { + rtc.close(); + rtc = null; + }, 500); +} + + +websocket.onmessage = async function (evt) { + const received_msg = evt.data; + const object = JSON.parse(received_msg); + if (object.type == "offer") { + document.getElementById('offer-sdp').textContent = object.sdp; + await handleOffer(object) + } +} diff --git a/examples/streamer/dispatchqueue.cpp b/examples/streamer/dispatchqueue.cpp new file mode 100644 index 0000000..f255fec --- /dev/null +++ b/examples/streamer/dispatchqueue.cpp @@ -0,0 +1,94 @@ +/* + * libdatachannel streamer example + * Copyright (c) 2020 Filip Klembara (in2core) + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; If not, see . + */ + + +#include "dispatchqueue.hpp" + +DispatchQueue::DispatchQueue(std::string name, size_t threadCount) : + name{std::move(name)}, threads(threadCount) { + for(size_t i = 0; i < threads.size(); i++) + { + threads[i] = std::thread(&DispatchQueue::dispatchThreadHandler, this); + } +} + +DispatchQueue::~DispatchQueue() { + // Signal to dispatch threads that it's time to wrap up + std::unique_lock lock(lockMutex); + quit = true; + lock.unlock(); + condition.notify_all(); + + // Wait for threads to finish before we exit + for(size_t i = 0; i < threads.size(); i++) + { + if(threads[i].joinable()) + { + threads[i].join(); + } + } +} + +void DispatchQueue::removePending() { + std::unique_lock lock(lockMutex); + queue = {}; +} + +void DispatchQueue::dispatch(const fp_t& op) { + std::unique_lock lock(lockMutex); + queue.push(op); + + // Manual unlocking is done before notifying, to avoid waking up + // the waiting thread only to block again (see notify_one for details) + lock.unlock(); + condition.notify_one(); +} + +void DispatchQueue::dispatch(fp_t&& op) { + std::unique_lock lock(lockMutex); + queue.push(std::move(op)); + + // Manual unlocking is done before notifying, to avoid waking up + // the waiting thread only to block again (see notify_one for details) + lock.unlock(); + condition.notify_one(); +} + +void DispatchQueue::dispatchThreadHandler(void) { + std::unique_lock lock(lockMutex); + do { + //Wait until we have data or a quit signal + condition.wait(lock, [this]{ + return (queue.size() || quit); + }); + + //after wait, we own the lock + if(!quit && queue.size()) + { + auto op = std::move(queue.front()); + queue.pop(); + + //unlock now that we're done messing with the queue + lock.unlock(); + + op(); + + lock.lock(); + } + } while (!quit); +} diff --git a/examples/streamer/dispatchqueue.hpp b/examples/streamer/dispatchqueue.hpp new file mode 100644 index 0000000..6e59e31 --- /dev/null +++ b/examples/streamer/dispatchqueue.hpp @@ -0,0 +1,56 @@ +/* + * libdatachannel streamer example + * Copyright (c) 2020 Filip Klembara (in2core) + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; If not, see . + */ + +#ifndef dispatchqueue_hpp +#define dispatchqueue_hpp + +#include +#include + +class DispatchQueue { + typedef std::function fp_t; + +public: + DispatchQueue(std::string name, size_t threadCount = 1); + ~DispatchQueue(); + + // dispatch and copy + void dispatch(const fp_t& op); + // dispatch and move + void dispatch(fp_t&& op); + + void removePending(); + + // Deleted operations + DispatchQueue(const DispatchQueue& rhs) = delete; + DispatchQueue& operator=(const DispatchQueue& rhs) = delete; + DispatchQueue(DispatchQueue&& rhs) = delete; + DispatchQueue& operator=(DispatchQueue&& rhs) = delete; + +private: + std::string name; + std::mutex lockMutex; + std::vector threads; + std::queue queue; + std::condition_variable condition; + bool quit = false; + + void dispatchThreadHandler(void); +}; + +#endif /* dispatchqueue_hpp */ diff --git a/examples/streamer/fileparser.cpp b/examples/streamer/fileparser.cpp new file mode 100644 index 0000000..f31579f --- /dev/null +++ b/examples/streamer/fileparser.cpp @@ -0,0 +1,59 @@ +/* + * libdatachannel streamer example + * Copyright (c) 2020 Filip Klembara (in2core) + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; If not, see . + */ + +#include "fileparser.hpp" +#include + +using namespace std; + +FileParser::FileParser(string directory, string extension, uint32_t samplesPerSecond, bool loop): sampleDuration_us(1000 * 1000 / samplesPerSecond), StreamSource() { + this->directory = directory; + this->extension = extension; + this->loop = loop; +} + +void FileParser::start() { + sampleTime_us = -sampleDuration_us; + loadNextSample(); +} + +void FileParser::stop() { + StreamSource::stop(); + counter = -1; +} + +void FileParser::loadNextSample() { + string frame_id = to_string(++counter); + + string url = directory + "/sample-" + frame_id + extension; + ifstream source(url, ios_base::binary); + if (!source) { + if (loop && counter > 0) { + loopTimestampOffset = sampleTime_us; + counter = -1; + loadNextSample(); + return; + } + sample = {}; + return; + } + + vector fileContents((std::istreambuf_iterator(source)), std::istreambuf_iterator()); + sample = *reinterpret_cast *>(&fileContents); + sampleTime_us += sampleDuration_us; +} diff --git a/examples/streamer/fileparser.hpp b/examples/streamer/fileparser.hpp new file mode 100644 index 0000000..4994996 --- /dev/null +++ b/examples/streamer/fileparser.hpp @@ -0,0 +1,40 @@ +/* + * libdatachannel streamer example + * Copyright (c) 2020 Filip Klembara (in2core) + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; If not, see . + */ + +#ifndef fileparser_hpp +#define fileparser_hpp + +#include +#include +#include "stream.hpp" + +class FileParser: public StreamSource { + std::string directory; + std::string extension; + uint32_t counter = -1; + bool loop; + uint64_t loopTimestampOffset = 0; +public: + const uint64_t sampleDuration_us; + virtual void start(); + virtual void stop(); + FileParser(std::string directory, std::string extension, uint32_t samplesPerSecond, bool loop); + virtual void loadNextSample(); +}; + +#endif /* fileparser_hpp */ diff --git a/examples/streamer/h264fileparser.cpp b/examples/streamer/h264fileparser.cpp new file mode 100644 index 0000000..d44c414 --- /dev/null +++ b/examples/streamer/h264fileparser.cpp @@ -0,0 +1,70 @@ +/* + * libdatachannel streamer example + * Copyright (c) 2020 Filip Klembara (in2core) + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; If not, see . + */ + +#include "h264fileparser.hpp" +#include +#include "rtc/rtc.hpp" + +using namespace std; + +H264FileParser::H264FileParser(string directory, uint32_t fps, bool loop): FileParser(directory, ".h264", fps, loop) { } + +void H264FileParser::loadNextSample() { + FileParser::loadNextSample(); + + unsigned long long i = 0; + while (i < sample.size()) { + assert(i + 4 < sample.size()); + auto lengthPtr = (uint32_t *) (sample.data() + i); + uint32_t length = ntohl(*lengthPtr); + auto naluStartIndex = i + 4; + auto naluEndIndex = naluStartIndex + length; + assert(naluEndIndex <= sample.size()); + auto header = reinterpret_cast(sample.data() + naluStartIndex); + auto type = header->unitType(); + switch (type) { + case 7: + previousUnitType7 = {sample.begin() + i, sample.begin() + naluEndIndex}; + break; + case 8: + previousUnitType8 = {sample.begin() + i, sample.begin() + naluEndIndex};; + break; + case 5: + previousUnitType5 = {sample.begin() + i, sample.begin() + naluEndIndex};; + break; + } + i = naluEndIndex; + } +} + +vector H264FileParser::initialNALUS() { + vector units{}; + if (previousUnitType7.has_value()) { + auto nalu = previousUnitType7.value(); + units.insert(units.end(), nalu.begin(), nalu.end()); + } + if (previousUnitType8.has_value()) { + auto nalu = previousUnitType8.value(); + units.insert(units.end(), nalu.begin(), nalu.end()); + } + if (previousUnitType5.has_value()) { + auto nalu = previousUnitType5.value(); + units.insert(units.end(), nalu.begin(), nalu.end()); + } + return units; +} diff --git a/examples/streamer/h264fileparser.hpp b/examples/streamer/h264fileparser.hpp new file mode 100644 index 0000000..ab79072 --- /dev/null +++ b/examples/streamer/h264fileparser.hpp @@ -0,0 +1,36 @@ +/* + * libdatachannel streamer example + * Copyright (c) 2020 Filip Klembara (in2core) + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; If not, see . + */ + +#ifndef h264fileparser_hpp +#define h264fileparser_hpp + +#include "fileparser.hpp" +#include + +class H264FileParser: public FileParser { + std::optional> previousUnitType5 = std::nullopt; + std::optional> previousUnitType7 = std::nullopt; + std::optional> previousUnitType8 = std::nullopt; + +public: + H264FileParser(std::string directory, uint32_t fps, bool loop); + void loadNextSample() override; + std::vector initialNALUS(); +}; + +#endif /* h264fileparser_hpp */ diff --git a/examples/streamer/helpers.cpp b/examples/streamer/helpers.cpp new file mode 100644 index 0000000..d17d8ca --- /dev/null +++ b/examples/streamer/helpers.cpp @@ -0,0 +1,49 @@ +/* + * libdatachannel streamer example + * Copyright (c) 2020 Filip Klembara (in2core) + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; If not, see . + */ + +#include "helpers.hpp" +#include + +using namespace std; +using namespace rtc; + +ClientTrackData::ClientTrackData(shared_ptr track, shared_ptr sender) { + this->track = track; + this->sender = sender; +} + +void Client::setState(State state) { + std::unique_lock lock(_mutex); + this->state = state; +} + +Client::State Client::getState() { + std::shared_lock lock(_mutex); + return state; +} + +ClientTrack::ClientTrack(string id, shared_ptr trackData) { + this->id = id; + this->trackData = trackData; +} + +uint64_t currentTimeInMicroSeconds() { + struct timeval time; + gettimeofday(&time, NULL); + return uint64_t(time.tv_sec) * 1000 * 1000 + time.tv_usec; +} diff --git a/examples/streamer/helpers.hpp b/examples/streamer/helpers.hpp new file mode 100644 index 0000000..557a897 --- /dev/null +++ b/examples/streamer/helpers.hpp @@ -0,0 +1,63 @@ +/* + * libdatachannel streamer example + * Copyright (c) 2020 Filip Klembara (in2core) + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; If not, see . + */ + +#ifndef helpers_hpp +#define helpers_hpp + +#include "rtc/rtc.hpp" + +struct ClientTrackData { + std::shared_ptr track; + std::shared_ptr sender; + + ClientTrackData(std::shared_ptr track, std::shared_ptr sender); +}; + +struct Client { + enum class State { + Waiting, + WaitingForVideo, + WaitingForAudio, + Ready + }; + const std::shared_ptr & peerConnection = _peerConnection; + Client(std::shared_ptr pc) { + _peerConnection = pc; + } + std::optional> video; + std::optional> audio; + std::optional> dataChannel{}; + void setState(State state); + State getState(); + +private: + std::shared_mutex _mutex; + State state = State::Waiting; + std::string id; + std::shared_ptr _peerConnection; +}; + +struct ClientTrack { + std::string id; + std::shared_ptr trackData; + ClientTrack(std::string id, std::shared_ptr trackData); +}; + +uint64_t currentTimeInMicroSeconds(); + +#endif /* helpers_hpp */ diff --git a/examples/streamer/index.html b/examples/streamer/index.html new file mode 100644 index 0000000..0e28a24 --- /dev/null +++ b/examples/streamer/index.html @@ -0,0 +1,73 @@ + + + + + libdatachannel media example + + + + + +

Options

+ +
+ + +
+ + + + +

State

+

+ ICE gathering state: +

+

+ ICE connection state: +

+

+ Signaling state: +

+ + + +

Data channel

+

+
+

SDP

+ +

Offer

+

+
+

Answer

+

+
+
+
+
+
diff --git a/examples/streamer/main.cpp b/examples/streamer/main.cpp
new file mode 100644
index 0000000..2fa9889
--- /dev/null
+++ b/examples/streamer/main.cpp
@@ -0,0 +1,473 @@
+/*
+ * libdatachannel client example
+ * Copyright (c) 2019-2020 Paul-Louis Ageneau
+ * Copyright (c) 2019 Murat Dogan
+ * Copyright (c) 2020 Will Munn
+ * Copyright (c) 2020 Nico Chatzi
+ * Copyright (c) 2020 Lara Mackey
+ * Copyright (c) 2020 Erik Cota-Robles
+ * Copyright (c) 2020 Filip Klembara (in2core)
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; If not, see .
+ */
+
+#include "nlohmann/json.hpp"
+
+#include "h264fileparser.hpp"
+#include "opusfileparser.hpp"
+#include "helpers.hpp"
+
+using namespace rtc;
+using namespace std;
+using namespace std::chrono_literals;
+
+using json = nlohmann::json;
+
+template  weak_ptr make_weak_ptr(shared_ptr ptr) { return ptr; }
+
+/// all connected clients
+unordered_map> clients{};
+
+/// Creates peer connection and client representation
+/// @param config Configuration
+/// @param wws Websocket for signaling
+/// @param id Client ID
+/// @returns Client
+shared_ptr createPeerConnection(const Configuration &config,
+                                        weak_ptr wws,
+                                        string id);
+
+/// Creates stream
+/// @param h264Samples Directory with H264 samples
+/// @param fps Video FPS
+/// @param opusSamples Directory with opus samples
+/// @returns Stream object
+shared_ptr createStream(const string h264Samples, const unsigned fps, const string opusSamples);
+
+/// Add client to stream
+/// @param client Client
+/// @param adding_video True if adding video
+void addToStream(shared_ptr client, bool isAddingVideo);
+
+/// Start stream
+void startStream();
+
+/// Main dispatch queue
+DispatchQueue MainThread("Main");
+
+/// Audio and video stream
+optional> avStream = nullopt;
+
+const string defaultRootDirectory = "../../../../examples/streamer/samples/";
+const string defaultH264SamplesDirectory = defaultRootDirectory + "h264/";
+string h264SamplesDirectory = defaultH264SamplesDirectory;
+const string defaultOpusSamplesDirectory = defaultRootDirectory + "opus/";
+string opusSamplesDirectory = defaultOpusSamplesDirectory;
+const string defaultIPAddress = "127.0.0.1";
+const uint16_t defaultPort = 8000;
+string ip_address = defaultIPAddress;
+uint16_t port = defaultPort;
+
+/// Incomming message handler for websocket
+/// @param message Incommint message
+/// @param config Configuration
+/// @param ws Websocket
+void wsOnMessage(json message, Configuration config, shared_ptr ws) {
+    auto it = message.find("id");
+    if (it == message.end())
+        return;
+    string id = it->get();
+    it = message.find("type");
+    if (it == message.end())
+        return;
+    string type = it->get();
+
+    if (type == "streamRequest") {
+        shared_ptr c = createPeerConnection(config, make_weak_ptr(ws), id);
+        clients.emplace(id, c);
+    } else if (type == "answer") {
+        shared_ptr c;
+        if (auto jt = clients.find(id); jt != clients.end()) {
+            auto pc = clients.at(id)->peerConnection;
+            auto sdp = message["sdp"].get();
+            auto description = Description(sdp, type);
+            pc->setRemoteDescription(description);
+        }
+    }
+}
+
+int main(int argc, char **argv) try {
+    bool enableDebugLogs = false;
+    bool printHelp = false;
+    int c = 0;
+    // parse arguments
+    while ((c = getopt (argc, argv, "a:b:d:p:vh")) != -1) {
+        switch (c) {
+            case 'a':
+                opusSamplesDirectory = string(optarg) + "/";
+                break;
+            case 'b':
+                h264SamplesDirectory = string(optarg) + "/";
+                break;
+            case 'd':
+                ip_address = string(optarg);
+                break;
+            case 'p':
+                port = atoi(optarg);
+                break;
+            case 'v':
+                enableDebugLogs = true;
+                break;
+            case 'h':
+                printHelp = true;
+                break;
+            case '?':
+                if (optopt == 'a' || optopt == 'b' || optopt == 'd' || optopt == 'p') {
+                    string s(1, optopt);
+                    cerr << "Option -" << s <<" requires an argument." << endl;
+                } else if (isprint(optopt)) {
+                    string s(1, optopt);
+                    cerr << "Unknown option `-" << s << "'." << endl;
+                } else {
+                    fprintf(stderr, "Unknown option character `\\x%x'.\n", optopt);
+                }
+                return 1;
+            default:
+                abort();
+        }
+    }
+    if (printHelp) {
+        cout << "usage: stream-h264 [-a opus_samples_folder] [-b h264_samples_folder] [-d ip_address] [-p port] [-v] [-h]" << endl
+        << "Arguments:" << endl
+        << "\t -a " << "Directory with opus samples (default: " << defaultOpusSamplesDirectory << ")." << endl
+        << "\t -b " << "Directory with H264 samples (default: " << defaultH264SamplesDirectory << ")." << endl
+        << "\t -d " << "Signaling server IP address (default: " << defaultIPAddress << ")." << endl
+        << "\t -p " << "Signaling server port (default: " << defaultPort << ")." << endl
+        << "\t -v " << "Enable debug logs." << endl
+        << "\t -h " << "Print this help and exit." << endl;
+        return 0;
+    }
+    if (enableDebugLogs) {
+        InitLogger(LogLevel::Debug);
+    }
+
+    Configuration config;
+    string stunServer = "stun:stun.l.google.com:19302";
+    cout << "Stun server is " << stunServer << endl;
+    config.iceServers.emplace_back(stunServer);
+
+
+    string localId = "server";
+    cout << "The local ID is: " << localId << endl;
+
+    auto ws = make_shared();
+    ws->onOpen([]() { cout << "WebSocket connected, signaling ready" << endl; });
+
+    ws->onClosed([]() { cout << "WebSocket closed" << endl; });
+
+    ws->onError([](const string &error) { cout << "WebSocket failed: " << error << endl; });
+
+    ws->onMessage([&](variant data) {
+        if (!holds_alternative(data))
+            return;
+
+        json message = json::parse(get(data));
+        MainThread.dispatch([message, config, ws]() {
+            wsOnMessage(message, config, ws);
+        });
+    });
+
+    const string url = "ws://" + ip_address + ":" + to_string(port) + "/" + localId;
+    cout << "Url is " << url << endl;
+    ws->open(url);
+
+    cout << "Waiting for signaling to be connected..." << endl;
+    while (!ws->isOpen()) {
+        if (ws->isClosed())
+            return 1;
+        this_thread::sleep_for(100ms);
+    }
+
+    while (true) {
+        string id;
+        cout << "Enter to exit" << endl;
+        cin >> id;
+        cin.ignore();
+        cout << "exiting" << endl;
+        break;
+    }
+
+    cout << "Cleaning up..." << endl;
+    return 0;
+
+} catch (const std::exception &e) {
+    std::cout << "Error: " << e.what() << std::endl;
+    return -1;
+}
+
+shared_ptr addVideo(const shared_ptr pc, const uint8_t payloadType, const uint32_t ssrc, const string cname, const string msid, const function onOpen) {
+    auto video = Description::Video(cname);
+    video.addH264Codec(payloadType);
+    video.addSSRC(ssrc, cname, msid);
+    auto track = pc->addTrack(video);
+    // create RTP configuration
+    auto rtpConfig = shared_ptr(new RTPPacketizationConfig(ssrc, cname, payloadType, H264RTPPacketizer::defaultClockRate));
+    // create packetizer
+    auto packetizer = shared_ptr(new H264RTPPacketizer(rtpConfig));
+    // create H264 and RTCP SP handler
+    shared_ptr h264Handler(new H264PacketizationHandler(H264PacketizationHandler::Separator::Length, packetizer));
+    // set handler
+    track->setRtcpHandler(h264Handler);
+    track->onOpen(onOpen);
+    auto trackData = make_shared(track, h264Handler);
+    return trackData;
+}
+
+shared_ptr addAudio(const shared_ptr pc, const uint8_t payloadType, const uint32_t ssrc, const string cname, const string msid, const function onOpen) {
+    auto audio = Description::Audio(cname);
+    audio.addOpusCodec(payloadType);
+    audio.addSSRC(ssrc, cname, msid);
+    auto track = pc->addTrack(audio);
+    // create RTP configuration
+    auto rtpConfig = shared_ptr(new RTPPacketizationConfig(ssrc, cname, payloadType, OpusRTPPacketizer::defaultClockRate));
+    // create packetizer
+    auto packetizer = make_shared(rtpConfig);
+    // create opus and RTCP SP handler
+    auto opusHandler = make_shared(packetizer);
+    // set handler
+    track->setRtcpHandler(opusHandler);
+    track->onOpen(onOpen);
+    auto trackData = make_shared(track, opusHandler);
+    return trackData;
+}
+
+// Create and setup a PeerConnection
+shared_ptr createPeerConnection(const Configuration &config,
+                                                weak_ptr wws,
+                                                string id) {
+
+    auto pc = make_shared(config);
+    shared_ptr client(new Client(pc));
+
+    pc->onStateChange([id](PeerConnection::State state) {
+        cout << "State: " << state << endl;
+        if (state == PeerConnection::State::Disconnected ||
+            state == PeerConnection::State::Failed ||
+            state == PeerConnection::State::Closed) {
+            // remove disconnected client
+            MainThread.dispatch([id]() {
+                clients.erase(id);
+            });
+        }
+    });
+
+    pc->onGatheringStateChange(
+        [wpc = make_weak_ptr(pc), id, wws](PeerConnection::GatheringState state) {
+        cout << "Gathering State: " << state << endl;
+        if (state == PeerConnection::GatheringState::Complete) {
+            if(auto pc = wpc.lock()) {
+                auto description = pc->localDescription();
+                json message = {
+                    {"id", id},
+                    {"type", description->typeString()},
+                    {"sdp", string(description.value())}
+                };
+                // Gathering complete, send answer
+                if (auto ws = wws.lock()) {
+                    ws->send(message.dump());
+                }
+            }
+        }
+    });
+
+    client->video = addVideo(pc, 102, 1, "video-stream", "stream1", [id, wc = make_weak_ptr(client)]() {
+        MainThread.dispatch([wc]() {
+            if (auto c = wc.lock()) {
+                addToStream(c, true);
+            }
+        });
+        cout << "Video from " << id << " opened" << endl;
+    });
+
+    client->audio = addAudio(pc, 111, 2, "audio-stream", "stream1", [id, wc = make_weak_ptr(client)]() {
+        MainThread.dispatch([wc]() {
+            if (auto c = wc.lock()) {
+                addToStream(c, false);
+            }
+        });
+        cout << "Audio from " << id << " opened" << endl;
+    });
+
+    auto dc = pc->addDataChannel("ping-pong");
+    dc->onOpen([id, wdc = make_weak_ptr(dc)]() {
+        if (auto dc = wdc.lock()) {
+            dc->send("Ping");
+        }
+    });
+
+    dc->onMessage(nullptr, [id, wdc = make_weak_ptr(dc)](string msg) {
+        cout << "Message from " << id << " received: " << msg << endl;
+        if (auto dc = wdc.lock()) {
+            dc->send("Ping");
+        }
+    });
+    client->dataChannel = dc;
+
+    pc->setLocalDescription();
+    return client;
+};
+
+/// Create stream
+shared_ptr createStream(const string h264Samples, const unsigned fps, const string opusSamples) {
+    // video source
+    auto video = make_shared(h264Samples, fps, true);
+    // audio source
+    auto audio = make_shared(opusSamples, true);
+
+    auto stream = make_shared(video, audio);
+    // set callback responsible for sample sending
+    stream->onSample([ws = make_weak_ptr(stream)](Stream::StreamSourceType type, uint64_t sampleTime, rtc::binary sample) {
+        vector tracks{};
+        string streamType = type == Stream::StreamSourceType::Video ? "video" : "audio";
+        // get track for given type
+        function> (shared_ptr)> getTrackData = [type](shared_ptr client) {
+            return type == Stream::StreamSourceType::Video ? client->video : client->audio;
+        };
+        // get all clients with Ready state
+        for(auto id_client: clients) {
+            auto id = id_client.first;
+            auto client = id_client.second;
+            auto optTrackData = getTrackData(client);
+            if (client->getState() == Client::State::Ready && optTrackData.has_value()) {
+                auto trackData = optTrackData.value();
+                tracks.push_back(ClientTrack(id, trackData));
+            }
+        }
+        if (!tracks.empty()) {
+            auto message = make_message(move(sample));
+            for (auto clientTrack: tracks) {
+                auto client = clientTrack.id;
+                auto trackData = clientTrack.trackData;
+                // sample time is in us, we need to convert it to seconds
+                auto elapsedSeconds = double(sampleTime) / (1000 * 1000);
+                auto rtpConfig = trackData->sender->rtpConfig;
+                // get elapsed time in clock rate
+                uint32_t elapsedTimestamp = rtpConfig->secondsToTimestamp(elapsedSeconds);
+
+                // set new timestamp
+                rtpConfig->timestamp = rtpConfig->startTimestamp + elapsedTimestamp;
+
+                // get elapsed time in clock rate from last RTCP sender report
+                auto reportElapsedTimestamp = rtpConfig->timestamp - trackData->sender->previousReportedTimestamp;
+                // check if last report was at least 1 second ago
+                if (rtpConfig->timestampToSeconds(reportElapsedTimestamp) > 1) {
+                    trackData->sender->setNeedsToReport();
+                }
+                cout << "Sending " << streamType << " sample with size: " << to_string(message->size()) << " to " << client << endl;
+                bool send = false;
+                try {
+                    // send sample
+                    send = trackData->track->send(*message);
+                } catch (...) {
+                    send = false;
+                }
+                if (!send) {
+                    cerr << "Unable to send "<< streamType << " packet" << endl;
+                    break;
+                }
+            }
+        }
+        MainThread.dispatch([ws]() {
+            if (clients.empty()) {
+                // we have no clients, stop the stream
+                if (auto stream = ws.lock()) {
+                    stream->stop();
+                }
+            }
+        });
+    });
+    return stream;
+}
+
+/// Start stream
+void startStream() {
+    shared_ptr stream;
+    if (avStream.has_value()) {
+        stream = avStream.value();
+        if (stream->isRunning) {
+            // stream is already running
+            return;
+        }
+    } else {
+        stream = createStream(h264SamplesDirectory, 30, opusSamplesDirectory);
+        avStream = stream;
+    }
+    stream->start();
+}
+
+/// Send previous key frame so browser can show something to user
+/// @param stream Stream
+/// @param video Video track data
+void sendInitialNalus(shared_ptr stream, shared_ptr video) {
+    auto h264 = dynamic_cast(stream->video.get());
+    auto initialNalus = h264->initialNALUS();
+
+    // send previous NALU key frame so users don't have to wait to see stream works
+    if (!initialNalus.empty()) {
+        const double frameDuration_s = double(h264->sampleDuration_us) / (1000 * 1000);
+        const uint32_t frameTimestampDuration = video->sender->rtpConfig->secondsToTimestamp(frameDuration_s);
+        video->sender->rtpConfig->timestamp = video->sender->rtpConfig->startTimestamp - frameTimestampDuration * 2;
+        video->track->send(initialNalus);
+        video->sender->rtpConfig->timestamp += frameTimestampDuration;
+        // Send initial NAL units again to start stream in firefox browser
+        video->track->send(initialNalus);
+    }
+}
+
+/// Add client to stream
+/// @param client Client
+/// @param adding_video True if adding video
+void addToStream(shared_ptr client, bool isAddingVideo) {
+    if (client->getState() == Client::State::Waiting) {
+        client->setState(isAddingVideo ? Client::State::WaitingForAudio : Client::State::WaitingForVideo);
+    } else if ((client->getState() == Client::State::WaitingForAudio && !isAddingVideo)
+               || (client->getState() == Client::State::WaitingForVideo && isAddingVideo)) {
+
+        // Audio and video tracks are collected now
+        assert(client->video.has_value() && client->audio.has_value());
+
+        auto video = client->video.value();
+        auto audio = client->audio.value();
+
+        auto currentTime_us = double(currentTimeInMicroSeconds());
+        auto currentTime_s = currentTime_us / (1000 * 1000);
+
+        // set start time of stream
+        video->sender->rtpConfig->setStartTime(currentTime_s, RTPPacketizationConfig::EpochStart::T1970);
+        audio->sender->rtpConfig->setStartTime(currentTime_s, RTPPacketizationConfig::EpochStart::T1970);
+
+        // start stat recording of RTCP SR
+        video->sender->startRecording();
+        audio->sender->startRecording();
+
+        if (avStream.has_value()) {
+            sendInitialNalus(avStream.value(), video);
+        }
+
+        client->setState(Client::State::Ready);
+    }
+    if (client->getState() == Client::State::Ready) {
+        startStream();
+    }
+}
diff --git a/examples/streamer/opusfileparser.cpp b/examples/streamer/opusfileparser.cpp
new file mode 100644
index 0000000..6204e01
--- /dev/null
+++ b/examples/streamer/opusfileparser.cpp
@@ -0,0 +1,23 @@
+/*
+ * libdatachannel streamer example
+ * Copyright (c) 2020 Filip Klembara (in2core)
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; If not, see .
+ */
+
+#include "opusfileparser.hpp"
+
+using namespace std;
+
+OPUSFileParser::OPUSFileParser(string directory, bool loop, uint32_t samplesPerSecond): FileParser(directory, ".opus", samplesPerSecond, loop) { }
diff --git a/examples/streamer/opusfileparser.hpp b/examples/streamer/opusfileparser.hpp
new file mode 100644
index 0000000..ffb96f9
--- /dev/null
+++ b/examples/streamer/opusfileparser.hpp
@@ -0,0 +1,32 @@
+/*
+ * libdatachannel streamer example
+ * Copyright (c) 2020 Filip Klembara (in2core)
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; If not, see .
+ */
+
+#ifndef opusfileparser_hpp
+#define opusfileparser_hpp
+
+#include "fileparser.hpp"
+
+class OPUSFileParser: public FileParser {
+    static const uint32_t defaultSamplesPerSecond = 50;
+
+public:
+    OPUSFileParser(std::string directory, bool loop, uint32_t samplesPerSecond = OPUSFileParser::defaultSamplesPerSecond);
+};
+
+
+#endif /* opusfileparser_hpp */
diff --git a/examples/streamer/stream.cpp b/examples/streamer/stream.cpp
new file mode 100644
index 0000000..b52536b
--- /dev/null
+++ b/examples/streamer/stream.cpp
@@ -0,0 +1,107 @@
+/*
+ * libdatachannel streamer example
+ * Copyright (c) 2020 Filip Klembara (in2core)
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; If not, see .
+ */
+
+#include "stream.hpp"
+#include "helpers.hpp"
+
+void StreamSource::stop() {
+    sampleTime_us = 0;
+    sample = {};
+}
+
+StreamSource::~StreamSource() {
+    stop();
+}
+
+Stream::Stream(std::shared_ptr video, std::shared_ptr audio): std::enable_shared_from_this(), video(video), audio(audio) { }
+
+Stream::~Stream() {
+    stop();
+}
+
+std::pair, Stream::StreamSourceType> Stream::unsafePrepareForSample() {
+    std::shared_ptr ss;
+    StreamSourceType sst;
+    uint64_t nextTime;
+    if (audio->getSampleTime_us() < video->getSampleTime_us()) {
+        ss = audio;
+        sst = StreamSourceType::Audio;
+        nextTime = audio->getSampleTime_us();
+    } else {
+        ss = video;
+        sst = StreamSourceType::Video;
+        nextTime = video->getSampleTime_us();
+    }
+
+    auto currentTime = currentTimeInMicroSeconds();
+
+    auto elapsed = currentTime - startTime;
+    if (nextTime > elapsed) {
+        auto waitTime = nextTime - elapsed;
+        mutex.unlock();
+        usleep(waitTime);
+        mutex.lock();
+    }
+    return {ss, sst};
+}
+
+void Stream::sendSample() {
+    std::lock_guard lock(mutex);
+    if (!isRunning) {
+        return;
+    }
+    auto ssSST = unsafePrepareForSample();
+    auto ss = ssSST.first;
+    auto sst = ssSST.second;
+    auto sample = ss->getSample();
+    sampleHandler(sst, ss->getSampleTime_us(), sample);
+    ss->loadNextSample();
+    dispatchQueue.dispatch([this]() {
+        this->sendSample();
+    });
+}
+
+void Stream::onSample(std::function handler) {
+    sampleHandler = handler;
+}
+
+void Stream::start() {
+    std::lock_guard lock(mutex);
+    if (isRunning) {
+        return;
+    }
+    _isRunning = true;
+    startTime = currentTimeInMicroSeconds();
+    audio->start();
+    video->start();
+    dispatchQueue.dispatch([this]() {
+        this->sendSample();
+    });
+}
+
+void Stream::stop() {
+    std::lock_guard lock(mutex);
+    if (!isRunning) {
+        return;
+    }
+    _isRunning = false;
+    dispatchQueue.removePending();
+    audio->stop();
+    video->stop();
+};
+
diff --git a/examples/streamer/stream.hpp b/examples/streamer/stream.hpp
new file mode 100644
index 0000000..8a80552
--- /dev/null
+++ b/examples/streamer/stream.hpp
@@ -0,0 +1,73 @@
+/*
+ * libdatachannel streamer example
+ * Copyright (c) 2020 Filip Klembara (in2core)
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; If not, see .
+ */
+
+#ifndef stream_hpp
+#define stream_hpp
+
+#include "dispatchqueue.hpp"
+#include "rtc/rtc.hpp"
+
+class StreamSource {
+protected:
+    uint64_t sampleTime_us = 0;
+    rtc::binary sample = {};
+
+public:
+    StreamSource() { }
+    virtual void start() = 0;
+    virtual void stop();
+    virtual void loadNextSample() = 0;
+
+    inline uint64_t getSampleTime_us() { return sampleTime_us; }
+    inline rtc::binary getSample() { return sample; }
+
+    ~StreamSource();
+};
+
+class Stream: std::enable_shared_from_this {
+    uint64_t startTime = 0;
+    std::mutex mutex;
+    DispatchQueue dispatchQueue = DispatchQueue("StreamQueue");
+
+    bool _isRunning = false;
+public:
+    const std::shared_ptr audio;
+    const std::shared_ptr video;
+    Stream(std::shared_ptr video, std::shared_ptr audio);
+    enum class StreamSourceType {
+        Audio,
+        Video
+    };
+    ~Stream();
+
+private:
+    rtc::synchronized_callback sampleHandler;
+
+    std::pair, StreamSourceType> unsafePrepareForSample();
+
+    void sendSample();
+
+public:
+    void onSample(std::function handler);
+    void start();
+    void stop();
+    const bool & isRunning = _isRunning;
+};
+
+
+#endif /* stream_hpp */