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