Initial commit

This commit is contained in:
Mathieu Duponchelle 2021-10-05 23:28:05 +02:00
commit 79fb66f338
17 changed files with 5163 additions and 0 deletions

2
.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
/target
*.dot

1672
Cargo.lock generated Normal file

File diff suppressed because it is too large Load diff

5
Cargo.toml Normal file
View file

@ -0,0 +1,5 @@
[workspace]
members = [
"plugins",
]

19
LICENSE Normal file
View file

@ -0,0 +1,19 @@
MIT License
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE

120
README.md Normal file
View file

@ -0,0 +1,120 @@
# webrtcsink
All-batteries included GStreamer WebRTC producer, that tries its best to do The Right Thing™.
## Use case
The [webrtcbin] element in GStreamer is extremely flexible and powerful, but using
it can be a difficult exercise. When all you want to do is serve a fixed set of streams
to any number of consumers, `webrtcsink` (which wraps `webrtcbin` internally) can be a
useful alternative.
[webrtcbin]: https://gstreamer.freedesktop.org/documentation/webrtc/index.html
## Features
`webrtcsink` implements the following features:
* Built-in signaller: when using the default signalling server (provided as a python
script [here](signalling/simple-server.py)), this element will perform signalling without
requiring application interaction. This makes it usable directly from `gst-launch`.
* Application-provided signalling: `webrtcsink` can be instantiated by an application
with a custom signaller. That signaller must be a GObject, and must implement the
`Signallable` interface as defined [here](plugins/src/webrtcsink/mod.rs). The
[default signaller](plugins/src/signaller/mod.rs) can be used as an example.
* Sandboxed consumers: when a consumer is added, its encoder / payloader / webrtcbin
elements run in a separately managed pipeline. This provides a certain level of
sandboxing, as opposed to having those elements running inside the element itself.
It is important to note that at this moment, encoding is not shared between consumers.
While this is not on the roadmap at the moment, nothing in the design prevents
implementing this optimization.
* Configuration: the level of user control over the element is at the moment quite
narrow, as the only interface exposed is control over proposed codecs, as well
as their order of priority. Consult `gst-inspect=1.0` for more information.
More features are on the roadmap, focusing on mechanisms for mitigating packet
loss and congestion.
It is important to note that full control over the individual elements used by
`webrtcsink` is *not* on the roadmap, as it will act as a black box in that respect,
for example `webrtcsink` wants to reserve control over the bitrate for congestion
control.
If more granular control is required, applications should use `webrtcbin` directly,
`webrtcsink` will focus on trying to just do the right thing, although it might
expose interfaces to guide and tune the heuristics it employs.
## Building
### Prerequisites
The element has only been tested for now against GStreamer master, with an
extra Merge Request pending review:
<https://gitlab.freedesktop.org/gstreamer/gstreamer/-/merge_requests/1233>
The MR should hopefully make it in in time for GStreamer's 1.20 release,
in the meantime the patches must be applied locally.
For testing, it is recommended to simply build GStreamer locally and run
in the uninstalled devenv.
> Make sure to install the development packages for some codec libraries
> beforehand, such as libx264, libvpx and libopusenc, exact names depend
> on your distribution.
```
git clone https://gitlab.freedesktop.org/meh/gstreamer/-/tree/webrtcsink
meson build
ninja -C build
ninja -C build devenv
```
### Compiling
``` shell
cargo build
```
## Usage
Open three terminals. In the first, run:
``` shell
cd signalling
python3 simple-server.py --addr=127.0.0.1 --disable-ssl
```
In the second, run:
``` shell
cd www
python3 -m http.server
```
In the third, run:
```
export GST_PLUGIN_PATH=$PWD/target/debug:$GST_PLUGIN_PATH
gst-launch-1.0 webrtcsink name=ws videotestsrc ! ws. audiotestsrc ! ws.
```
When the pipeline above is running succesfully, open a browser and
point it to the python server:
```
xdg-open http://127.0.0.1:8000
```
You should see an identifier listed in the left-hand panel, click on
it. You should see a test video stream, and hear a test tone.
## License
All code in this repository is licensed under the [MIT license].
[MIT license]: https://opensource.org/licenses/MIT

51
plugins/Cargo.toml Normal file
View file

@ -0,0 +1,51 @@
[package]
name = "webrtcsink"
version = "0.1.0"
edition = "2018"
authors = ["Mathieu Duponchelle <mathieu@centricular.com>"]
license = "MIT"
description = "GStreamer WebRTC sink"
repository = "https://github.com/centricular/webrtcsink/"
build = "build.rs"
[dependencies]
gst = { package = "gstreamer", git = "https://gitlab.freedesktop.org/gstreamer/gstreamer-rs", features = ["v1_20"] }
gst-app = { package = "gstreamer-app", git = "https://gitlab.freedesktop.org/gstreamer/gstreamer-rs", features = ["v1_20"] }
gst-video = { package = "gstreamer-video", git = "https://gitlab.freedesktop.org/gstreamer/gstreamer-rs", features = ["v1_20"] }
gst-webrtc = { package = "gstreamer-webrtc", git = "https://gitlab.freedesktop.org/gstreamer/gstreamer-rs", features = ["v1_20"] }
gst-sdp = { package = "gstreamer-sdp", git = "https://gitlab.freedesktop.org/gstreamer/gstreamer-rs", features = ["v1_20"] }
once_cell = "1.0"
smallvec = "1"
anyhow = "1"
futures = "0.3"
async-std = "1"
async-tungstenite = { version = "0.10", features = ["async-std-runtime", "async-native-tls"] }
serde = "1"
serde_derive = "1"
serde_json = "1"
fastrand = "1.0"
[lib]
name = "webrtcsink"
crate-type = ["cdylib", "rlib"]
path = "src/lib.rs"
[build-dependencies]
gst-plugin-version-helper = "0.7"
[features]
static = []
capi = []
[package.metadata.capi]
min_version = "0.8.0"
[package.metadata.capi.header]
enabled = false
[package.metadata.capi.library]
install_subdir = "gstreamer-1.0"
versioning = false
[package.metadata.capi.pkg_config]
requires_private = "gstreamer-webrtc >= 1.20, gstreamer-1.0 >= 1.20, gstreamer-app >= 1.20, gstreamer-video >= 1.20, gstreamer-sdp >= 1.20, gobject-2.0, glib-2.0, gmodule-2.0"

3
plugins/build.rs Normal file
View file

@ -0,0 +1,3 @@
fn main() {
gst_plugin_version_helper::info()
}

22
plugins/src/lib.rs Normal file
View file

@ -0,0 +1,22 @@
use gst::glib;
mod signaller;
pub mod webrtcsink;
fn plugin_init(plugin: &gst::Plugin) -> Result<(), glib::BoolError> {
webrtcsink::register(plugin)?;
Ok(())
}
gst::plugin_define!(
webrtcsink,
env!("CARGO_PKG_DESCRIPTION"),
plugin_init,
concat!(env!("CARGO_PKG_VERSION"), "-", env!("COMMIT_ID")),
"MIT",
env!("CARGO_PKG_NAME"),
env!("CARGO_PKG_NAME"),
env!("CARGO_PKG_REPOSITORY"),
env!("BUILD_REL_DATE")
);

View file

@ -0,0 +1,393 @@
use crate::webrtcsink::WebRTCSink;
use anyhow::{anyhow, Error};
use async_std::task;
use async_tungstenite::tungstenite::Message as WsMessage;
use futures::channel::mpsc;
use futures::prelude::*;
use gst::glib;
use gst::glib::prelude::*;
use gst::subclass::prelude::*;
use gst::{gst_debug, gst_error, gst_info, gst_trace, gst_warning};
use once_cell::sync::Lazy;
use serde_derive::{Deserialize, Serialize};
use std::sync::Mutex;
static CAT: Lazy<gst::DebugCategory> = Lazy::new(|| {
gst::DebugCategory::new(
"webrtcsink-signaller",
gst::DebugColorFlags::empty(),
Some("WebRTC sink signaller"),
)
});
#[derive(Default)]
struct State {
/// Sender for the websocket messages
websocket_sender: Option<mpsc::Sender<WsMessage>>,
send_task_handle: Option<task::JoinHandle<Result<(), Error>>>,
receive_task_handle: Option<task::JoinHandle<()>>,
}
#[derive(Serialize, Deserialize)]
#[serde(tag = "type")]
#[serde(rename_all = "lowercase")]
enum SdpMessage {
Offer { sdp: String },
Answer { sdp: String },
}
#[derive(Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
enum JsonMsgInner {
Ice {
candidate: String,
#[serde(rename = "sdpMLineIndex")]
sdp_mline_index: u32,
},
Sdp(SdpMessage),
}
#[derive(Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
struct JsonMsg {
#[serde(rename = "peer-id")]
peer_id: String,
#[serde(flatten)]
inner: JsonMsgInner,
}
struct Settings {
address: Option<String>,
}
impl Default for Settings {
fn default() -> Self {
Self {
address: Some("ws://127.0.0.1:8443".to_string()),
}
}
}
#[derive(Default)]
pub struct Signaller {
state: Mutex<State>,
settings: Mutex<Settings>,
}
impl Signaller {
async fn connect(&self, element: &WebRTCSink) -> Result<(), Error> {
let address = self
.settings
.lock()
.unwrap()
.address
.as_ref()
.unwrap()
.clone();
let (ws, _) = async_tungstenite::async_std::connect_async(address).await?;
gst_info!(CAT, obj: element, "connected");
// Channel for asynchronously sending out websocket message
let (mut ws_sink, mut ws_stream) = ws.split();
ws_sink
.send(WsMessage::Text("REGISTER PRODUCER".to_string()))
.await?;
// 1000 is completely arbitrary, we simply don't want infinite piling
// up of messages as with unbounded
let (websocket_sender, mut websocket_receiver) = mpsc::channel::<WsMessage>(1000);
let element_clone = element.downgrade();
let send_task_handle = task::spawn(async move {
while let Some(msg) = websocket_receiver.next().await {
if let Some(element) = element_clone.upgrade() {
gst_trace!(CAT, obj: &element, "Sending websocket message {:?}", msg);
}
ws_sink.send(msg).await?;
}
if let Some(element) = element_clone.upgrade() {
gst_info!(CAT, obj: &element, "Done sending");
}
ws_sink.send(WsMessage::Close(None)).await?;
ws_sink.close().await?;
Ok::<(), Error>(())
});
let element_clone = element.downgrade();
let receive_task_handle = task::spawn(async move {
while let Some(msg) = async_std::stream::StreamExt::next(&mut ws_stream).await {
if let Some(element) = element_clone.upgrade() {
match msg {
Ok(WsMessage::Text(msg)) => {
gst_trace!(CAT, obj: &element, "Received message {}", msg);
if msg.starts_with("REGISTERED ") {
gst_info!(CAT, obj: &element, "We are registered with the server");
} else if let Some(peer_id) = msg.strip_prefix("START_SESSION ") {
if let Err(err) = element.add_consumer(peer_id) {
gst_warning!(CAT, obj: &element, "{}", err);
}
} else if let Some(peer_id) = msg.strip_prefix("END_SESSION ") {
if let Err(err) = element.remove_consumer(peer_id) {
gst_warning!(CAT, obj: &element, "{}", err);
}
} else if let Ok(msg) = serde_json::from_str::<JsonMsg>(&msg) {
match msg.inner {
JsonMsgInner::Sdp(SdpMessage::Answer { sdp }) => {
if let Err(err) = element.handle_sdp(
&msg.peer_id,
&gst_webrtc::WebRTCSessionDescription::new(
gst_webrtc::WebRTCSDPType::Answer,
gst_sdp::SDPMessage::parse_buffer(sdp.as_bytes())
.unwrap(),
),
) {
gst_warning!(CAT, obj: &element, "{}", err);
}
}
JsonMsgInner::Sdp(SdpMessage::Offer { .. }) => {
gst_warning!(
CAT,
obj: &element,
"Ignoring offer from peer"
);
}
JsonMsgInner::Ice {
candidate,
sdp_mline_index,
} => {
if let Err(err) = element.handle_ice(
&msg.peer_id,
Some(sdp_mline_index),
None,
&candidate,
) {
gst_warning!(CAT, obj: &element, "{}", err);
}
}
}
} else {
gst_error!(
CAT,
obj: &element,
"Unknown message from server: {}",
msg
);
element.handle_signalling_error(anyhow!(
"Unknown message from server: {}",
msg
));
}
}
Ok(WsMessage::Close(reason)) => {
gst_info!(
CAT,
obj: &element,
"websocket connection closed: {:?}",
reason
);
break;
}
Ok(_) => (),
Err(err) => {
element.handle_signalling_error(anyhow!("Error receiving: {}", err));
break;
}
}
} else {
break;
}
}
if let Some(element) = element_clone.upgrade() {
gst_info!(CAT, obj: &element, "Stopped websocket receiving");
}
});
let mut state = self.state.lock().unwrap();
state.websocket_sender = Some(websocket_sender);
state.send_task_handle = Some(send_task_handle);
state.receive_task_handle = Some(receive_task_handle);
Ok(())
}
pub fn start(&self, element: &WebRTCSink) {
let this = self.instance().clone();
let element_clone = element.clone();
task::spawn(async move {
let this = Self::from_instance(&this);
if let Err(err) = this.connect(&element_clone).await {
element_clone.handle_signalling_error(err);
}
});
}
pub fn handle_sdp(
&self,
element: &WebRTCSink,
peer_id: &str,
sdp: &gst_webrtc::WebRTCSessionDescription,
) {
let state = self.state.lock().unwrap();
let msg = JsonMsg {
peer_id: peer_id.to_string(),
inner: JsonMsgInner::Sdp(SdpMessage::Offer {
sdp: sdp.sdp().as_text().unwrap(),
}),
};
if let Some(mut sender) = state.websocket_sender.clone() {
let element = element.downgrade();
task::spawn(async move {
if let Err(err) = sender
.send(WsMessage::Text(serde_json::to_string(&msg).unwrap()))
.await
{
if let Some(element) = element.upgrade() {
element.handle_signalling_error(anyhow!("Error: {}", err));
}
}
});
}
}
pub fn handle_ice(
&self,
element: &WebRTCSink,
peer_id: &str,
candidate: &str,
sdp_mline_index: Option<u32>,
_sdp_mid: Option<String>,
) {
let state = self.state.lock().unwrap();
let msg = JsonMsg {
peer_id: peer_id.to_string(),
inner: JsonMsgInner::Ice {
candidate: candidate.to_string(),
sdp_mline_index: sdp_mline_index.unwrap(),
},
};
if let Some(mut sender) = state.websocket_sender.clone() {
let element = element.downgrade();
task::spawn(async move {
if let Err(err) = sender
.send(WsMessage::Text(serde_json::to_string(&msg).unwrap()))
.await
{
if let Some(element) = element.upgrade() {
element.handle_signalling_error(anyhow!("Error: {}", err));
}
}
});
}
}
pub fn stop(&self, element: &WebRTCSink) {
gst_info!(CAT, obj: element, "Stopping now");
let mut state = self.state.lock().unwrap();
let send_task_handle = state.send_task_handle.take();
let receive_task_handle = state.receive_task_handle.take();
if let Some(mut sender) = state.websocket_sender.take() {
task::block_on(async move {
sender.close_channel();
if let Some(handle) = send_task_handle {
if let Err(err) = handle.await {
gst_warning!(CAT, obj: element, "Error while joining send task: {}", err);
}
}
if let Some(handle) = receive_task_handle {
handle.await;
}
});
}
}
pub fn consumer_removed(&self, element: &WebRTCSink, peer_id: &str) {
gst_debug!(CAT, obj: element, "Signalling consumer {} removed", peer_id);
let state = self.state.lock().unwrap();
let peer_id = peer_id.to_string();
let element = element.downgrade();
if let Some(mut sender) = state.websocket_sender.clone() {
task::spawn(async move {
if let Err(err) = sender
.send(WsMessage::Text(format!("END_SESSION {}", peer_id)))
.await
{
if let Some(element) = element.upgrade() {
element.handle_signalling_error(anyhow!("Error: {}", err));
}
}
});
}
}
}
#[glib::object_subclass]
impl ObjectSubclass for Signaller {
const NAME: &'static str = "RsWebRTCSinkSignaller";
type Type = super::Signaller;
type ParentType = glib::Object;
}
impl ObjectImpl for Signaller {
fn properties() -> &'static [glib::ParamSpec] {
static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(|| {
vec![glib::ParamSpec::new_string(
"address",
"Address",
"Address of the signalling server",
Some("ws://127.0.0.1:8443"),
glib::ParamFlags::READWRITE,
)]
});
PROPERTIES.as_ref()
}
fn set_property(
&self,
_obj: &Self::Type,
_id: usize,
value: &glib::Value,
pspec: &glib::ParamSpec,
) {
match pspec.name() {
"address" => {
let address: Option<_> = value.get().expect("type checked upstream");
if let Some(address) = address {
gst_info!(CAT, "Signaller address set to {}", address);
let mut settings = self.settings.lock().unwrap();
settings.address = Some(address);
} else {
gst_error!(CAT, "address can't be None");
}
}
_ => unimplemented!(),
}
}
fn property(&self, _obj: &Self::Type, _id: usize, pspec: &glib::ParamSpec) -> glib::Value {
match pspec.name() {
"address" => {
let settings = self.settings.lock().unwrap();
settings.address.to_value()
}
_ => unimplemented!(),
}
}
}

View file

@ -0,0 +1,61 @@
use crate::webrtcsink::{Signallable, WebRTCSink};
use gst::glib;
use gst::subclass::prelude::ObjectSubclassExt;
mod imp;
glib::wrapper! {
pub struct Signaller(ObjectSubclass<imp::Signaller>);
}
unsafe impl Send for Signaller {}
unsafe impl Sync for Signaller {}
impl Signallable for Signaller {
fn start(&mut self, element: &WebRTCSink) -> Result<(), anyhow::Error> {
let signaller = imp::Signaller::from_instance(self);
signaller.start(element);
Ok(())
}
fn handle_sdp(
&mut self,
element: &WebRTCSink,
peer_id: &str,
sdp: &gst_webrtc::WebRTCSessionDescription,
) -> Result<(), anyhow::Error> {
let signaller = imp::Signaller::from_instance(self);
signaller.handle_sdp(element, peer_id, sdp);
Ok(())
}
fn handle_ice(
&mut self,
element: &WebRTCSink,
peer_id: &str,
candidate: &str,
sdp_mline_index: Option<u32>,
sdp_mid: Option<String>,
) -> Result<(), anyhow::Error> {
let signaller = imp::Signaller::from_instance(self);
signaller.handle_ice(element, peer_id, candidate, sdp_mline_index, sdp_mid);
Ok(())
}
fn stop(&mut self, element: &WebRTCSink) {
let signaller = imp::Signaller::from_instance(self);
signaller.stop(element);
}
fn consumer_removed(&mut self, element: &WebRTCSink, peer_id: &str) {
let signaller = imp::Signaller::from_instance(self);
signaller.consumer_removed(element, peer_id);
}
}
impl Signaller {
pub fn new() -> Self {
glib::Object::new(&[]).unwrap()
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,120 @@
use anyhow::Error;
use gst::glib;
use gst::prelude::*;
use gst::subclass::prelude::ObjectSubclassExt;
mod imp;
mod utils;
glib::wrapper! {
pub struct WebRTCSink(ObjectSubclass<imp::WebRTCSink>) @extends gst::Bin, gst::Element, gst::Object, @implements gst::ChildProxy;
}
unsafe impl Send for WebRTCSink {}
unsafe impl Sync for WebRTCSink {}
pub trait Signallable: Sync + Send + 'static {
fn start(&mut self, element: &WebRTCSink) -> Result<(), Error>;
fn handle_sdp(
&mut self,
element: &WebRTCSink,
peer_id: &str,
sdp: &gst_webrtc::WebRTCSessionDescription,
) -> Result<(), Error>;
/// sdp_mid is exposed for future proofing, see
/// https://gitlab.freedesktop.org/gstreamer/gst-plugins-bad/-/issues/1174,
/// at the moment sdp_mline_index will always be Some and sdp_mid will always
/// be None
fn handle_ice(
&mut self,
element: &WebRTCSink,
peer_id: &str,
candidate: &str,
sdp_mline_index: Option<u32>,
sdp_mid: Option<String>,
) -> Result<(), Error>;
fn consumer_removed(&mut self, element: &WebRTCSink, peer_id: &str);
fn stop(&mut self, element: &WebRTCSink);
}
/// When providing a signaller, we expect it to both be a GObject
/// and be Signallable. This is arguably a bit strange, but exposing
/// a GInterface from rust is at the moment a bit awkward, so I went
/// for a rust interface for now. The reason the signaller needs to be
/// a GObject is to make its properties available through the GstChildProxy
/// interface.
pub trait SignallableObject: AsRef<glib::Object> + Signallable {}
impl<T: AsRef<glib::Object> + Signallable> SignallableObject for T {}
impl WebRTCSink {
pub fn new() -> Self {
glib::Object::new(&[]).unwrap()
}
pub fn with_signaller(signaller: Box<dyn SignallableObject>) -> Self {
let ret: WebRTCSink = glib::Object::new(&[]).unwrap();
let ws = imp::WebRTCSink::from_instance(&ret);
ws.set_signaller(signaller).unwrap();
ret
}
pub fn handle_sdp(
&self,
peer_id: &str,
sdp: &gst_webrtc::WebRTCSessionDescription,
) -> Result<(), Error> {
let ws = imp::WebRTCSink::from_instance(self);
ws.handle_sdp(self, peer_id, sdp)
}
/// sdp_mid is exposed for future proofing, see
/// https://gitlab.freedesktop.org/gstreamer/gst-plugins-bad/-/issues/1174,
/// at the moment sdp_mline_index must be Some
pub fn handle_ice(
&self,
peer_id: &str,
sdp_mline_index: Option<u32>,
sdp_mid: Option<String>,
candidate: &str,
) -> Result<(), Error> {
let ws = imp::WebRTCSink::from_instance(self);
ws.handle_ice(self, peer_id, sdp_mline_index, sdp_mid, candidate)
}
pub fn handle_signalling_error(&self, error: anyhow::Error) {
let ws = imp::WebRTCSink::from_instance(self);
ws.handle_signalling_error(self, error);
}
pub fn add_consumer(&self, peer_id: &str) -> Result<(), Error> {
let ws = imp::WebRTCSink::from_instance(self);
ws.add_consumer(self, peer_id)
}
pub fn remove_consumer(&self, peer_id: &str) -> Result<(), Error> {
let ws = imp::WebRTCSink::from_instance(self);
ws.remove_consumer(self, peer_id, false)
}
}
pub fn register(plugin: &gst::Plugin) -> Result<(), glib::BoolError> {
gst::Element::register(
Some(plugin),
"webrtcsink",
gst::Rank::None,
WebRTCSink::static_type(),
)
}

View file

@ -0,0 +1,318 @@
use std::collections::HashMap;
use std::mem;
use std::sync::{atomic, Arc, Mutex};
use anyhow::{Context, Error};
use once_cell::sync::Lazy;
use gst::prelude::*;
use gst::{gst_debug, gst_error, gst_trace, gst_warning};
static CAT: Lazy<gst::DebugCategory> = Lazy::new(|| {
gst::DebugCategory::new(
"app-stream-producer",
gst::DebugColorFlags::empty(),
Some("gst_app Stream Producer interface"),
)
});
/// Wrapper around `gst::ElementFactory::make` with a better error
/// message
pub fn make_element(element: &str, name: Option<&str>) -> Result<gst::Element, Error> {
gst::ElementFactory::make(element, name)
.with_context(|| format!("Failed to make element {}", element))
}
/// The interface for transporting media data from one node
/// to another.
///
/// A producer is essentially a GStreamer `appsink` whose output
/// is sent to a set of consumers, who are essentially `appsrc` wrappers
#[derive(Debug, Clone)]
pub struct StreamProducer {
/// The appsink to dispatch data for
appsink: gst_app::AppSink,
/// The consumers to dispatch data to
consumers: Arc<Mutex<StreamConsumers>>,
}
impl PartialEq for StreamProducer {
fn eq(&self, other: &Self) -> bool {
self.appsink.eq(&other.appsink)
}
}
impl Eq for StreamProducer {}
impl StreamProducer {
/// Add an appsrc to dispatch data to
pub fn add_consumer(&self, consumer: &gst_app::AppSrc, consumer_id: &str) {
let mut consumers = self.consumers.lock().unwrap();
if consumers.consumers.get(consumer_id).is_some() {
gst_error!(CAT, "Consumer already added");
return;
}
gst_debug!(CAT, "Adding consumer");
consumer.set_property("max-buffers", 0u64).unwrap();
consumer.set_property("max-bytes", 0u64).unwrap();
consumer
.set_property("max-time", 500 * gst::ClockTime::MSECOND)
.unwrap();
consumer
.set_property_from_str("leaky-type", "downstream")
.unwrap();
// Forward force-keyunit events upstream to the appsink
let srcpad = consumer.static_pad("src").unwrap();
let appsink_clone = self.appsink.clone();
let fku_probe_id = srcpad
.add_probe(gst::PadProbeType::EVENT_UPSTREAM, move |_pad, info| {
if let Some(gst::PadProbeData::Event(ref ev)) = info.data {
if gst_video::UpstreamForceKeyUnitEvent::parse(ev).is_ok() {
gst_debug!(CAT, "Requesting keyframe");
let _ = appsink_clone.send_event(ev.clone());
}
}
gst::PadProbeReturn::Ok
})
.unwrap();
consumers.consumers.insert(
consumer_id.to_string(),
StreamConsumer::new(consumer, fku_probe_id, consumer_id),
);
}
/// Remove a consumer appsrc by id
pub fn remove_consumer(&self, consumer_id: &str) {
if let Some(consumer) = self.consumers.lock().unwrap().consumers.remove(consumer_id) {
gst_debug!(CAT, "Removed consumer {}", consumer.appsrc.name());
} else {
gst_debug!(CAT, "Consumer {} not found", consumer_id);
}
}
/// Stop discarding data samples and start forwarding them to the consumers.
///
/// This is useful for example for prerolling live sources.
pub fn forward(&self) {
self.consumers.lock().unwrap().discard = false;
}
/// Get the GStreamer `appsink` wrapped by this producer
pub fn appsink(&self) -> &gst_app::AppSink {
&self.appsink
}
}
impl<'a> From<&'a gst_app::AppSink> for StreamProducer {
fn from(appsink: &'a gst_app::AppSink) -> Self {
let consumers = Arc::new(Mutex::new(StreamConsumers {
current_latency: None,
latency_updated: false,
consumers: HashMap::new(),
discard: true,
}));
let consumers_clone = consumers.clone();
let consumers_clone2 = consumers.clone();
appsink.set_callbacks(
gst_app::AppSinkCallbacks::builder()
.new_sample(move |appsink| {
let mut consumers = consumers_clone.lock().unwrap();
let sample = match appsink.pull_sample() {
Ok(sample) => sample,
Err(_err) => {
gst_debug!(CAT, "Failed to pull sample");
return Err(gst::FlowError::Flushing);
}
};
if consumers.discard {
return Ok(gst::FlowSuccess::Ok);
}
gst_trace!(CAT, "processing sample");
let latency = consumers.current_latency;
let latency_updated = mem::replace(&mut consumers.latency_updated, false);
let mut requested_keyframe = false;
let current_consumers = consumers
.consumers
.values()
.map(|c| {
if let Some(latency) = latency {
if c.forwarded_latency
.compare_exchange(
false,
true,
atomic::Ordering::SeqCst,
atomic::Ordering::SeqCst,
)
.is_ok()
|| latency_updated
{
c.appsrc.set_latency(latency, gst::ClockTime::NONE);
}
}
if c.first_buffer
.compare_exchange(
true,
false,
atomic::Ordering::SeqCst,
atomic::Ordering::SeqCst,
)
.is_ok()
&& !requested_keyframe
{
gst_debug!(CAT, "Requesting keyframe for first buffer");
appsink.send_event(
gst_video::UpstreamForceKeyUnitEvent::builder()
.all_headers(true)
.build(),
);
requested_keyframe = true;
}
c.appsrc.clone()
})
.collect::<smallvec::SmallVec<[_; 16]>>();
drop(consumers);
for consumer in current_consumers {
if let Err(err) = consumer.push_sample(&sample) {
gst_warning!(CAT, "Failed to push sample: {}", err);
}
}
Ok(gst::FlowSuccess::Ok)
})
.eos(move |_| {
let current_consumers = consumers_clone2
.lock()
.unwrap()
.consumers
.values()
.map(|c| c.appsrc.clone())
.collect::<smallvec::SmallVec<[_; 16]>>();
for consumer in current_consumers {
let _ = consumer.end_of_stream();
}
})
.build(),
);
let consumers_clone = consumers.clone();
let sinkpad = appsink.static_pad("sink").unwrap();
sinkpad.add_probe(gst::PadProbeType::EVENT_UPSTREAM, move |pad, info| {
if let Some(gst::PadProbeData::Event(ref ev)) = info.data {
use gst::EventView;
if let EventView::Latency(ev) = ev.view() {
if pad.parent().is_some() {
let latency = ev.latency();
let mut consumers = consumers_clone.lock().unwrap();
consumers.current_latency = Some(latency);
consumers.latency_updated = true;
}
}
}
gst::PadProbeReturn::Ok
});
StreamProducer {
appsink: appsink.clone(),
consumers,
}
}
}
/// Wrapper around a HashMap of consumers, exists for thread safety
/// and also protects some of the producer state
#[derive(Debug)]
struct StreamConsumers {
/// The currently-observed latency
current_latency: Option<gst::ClockTime>,
/// Whether the consumers' appsrc latency needs updating
latency_updated: bool,
/// The consumers, link id -> consumer
consumers: HashMap<String, StreamConsumer>,
/// Whether appsrc samples should be forwarded to consumers yet
discard: bool,
}
/// Wrapper around a consumer's `appsrc`
#[derive(Debug)]
struct StreamConsumer {
/// The GStreamer `appsrc` of the consumer
appsrc: gst_app::AppSrc,
/// The id of a pad probe that intercepts force-key-unit events
fku_probe_id: Option<gst::PadProbeId>,
/// Whether an initial latency was forwarded to the `appsrc`
forwarded_latency: atomic::AtomicBool,
/// Whether a first buffer has made it through, used to determine
/// whether a new key unit should be requested. Only useful for encoded
/// streams.
first_buffer: atomic::AtomicBool,
}
impl StreamConsumer {
/// Create a new consumer
fn new(appsrc: &gst_app::AppSrc, fku_probe_id: gst::PadProbeId, consumer_id: &str) -> Self {
let consumer_id = consumer_id.to_string();
appsrc.set_callbacks(
gst_app::AppSrcCallbacks::builder()
.enough_data(move |_appsrc| {
gst_debug!(
CAT,
"consumer {} is not consuming fast enough, old samples are getting dropped",
consumer_id
);
})
.build(),
);
StreamConsumer {
appsrc: appsrc.clone(),
fku_probe_id: Some(fku_probe_id),
forwarded_latency: atomic::AtomicBool::new(false),
first_buffer: atomic::AtomicBool::new(true),
}
}
}
impl Drop for StreamConsumer {
fn drop(&mut self) {
if let Some(fku_probe_id) = self.fku_probe_id.take() {
let srcpad = self.appsrc.static_pad("src").unwrap();
srcpad.remove_probe(fku_probe_id);
}
}
}
impl PartialEq for StreamConsumer {
fn eq(&self, other: &Self) -> bool {
self.appsrc.eq(&other.appsrc)
}
}
impl Eq for StreamConsumer {}
impl std::hash::Hash for StreamConsumer {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
std::hash::Hash::hash(&self.appsrc, state);
}
}
impl std::borrow::Borrow<gst_app::AppSrc> for StreamConsumer {
fn borrow(&self) -> &gst_app::AppSrc {
&self.appsrc
}
}

240
signalling/simple-server.py Executable file
View file

@ -0,0 +1,240 @@
#!/usr/bin/env python3
#
# Example 1-1 call signalling server
#
# Copyright (C) 2017 Centricular Ltd.
#
# Author: Nirbheek Chauhan <nirbheek@centricular.com>
#
import os
import sys
import ssl
import logging
import asyncio
import websockets
import argparse
import json
import uuid
import concurrent
from collections import defaultdict
from concurrent.futures._base import TimeoutError
import logging
parser = argparse.ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFormatter)
parser.add_argument('--addr', default='0.0.0.0', help='Address to listen on')
parser.add_argument('--port', default=8443, type=int, help='Port to listen on')
parser.add_argument('--keepalive-timeout', dest='keepalive_timeout', default=30, type=int, help='Timeout for keepalive (in seconds)')
parser.add_argument('--disable-ssl', default=False, help='Disable ssl', action='store_true')
options = parser.parse_args(sys.argv[1:])
ADDR_PORT = (options.addr, options.port)
KEEPALIVE_TIMEOUT = options.keepalive_timeout
############### Global data ###############
# Format: {uid: (Peer WebSocketServerProtocol,
# remote_address,
# [<'session'>,]}
peers = dict()
# Format: {caller_uid: [callee_uid,],
# callee_uid: [caller_uid,]}
# Bidirectional mapping between the two peers
sessions = defaultdict(list)
producers = set()
consumers = set()
listeners = set()
############### Helper functions ###############
async def recv_msg_ping(ws, raddr):
'''
Wait for a message forever, and send a regular ping to prevent bad routers
from closing the connection.
'''
msg = None
while msg is None:
try:
msg = await asyncio.wait_for(ws.recv(), KEEPALIVE_TIMEOUT)
except (asyncio.TimeoutError, concurrent.futures._base.TimeoutError):
print('Sending keepalive ping to {!r} in recv'.format(raddr))
await ws.ping()
return msg
async def cleanup_session(uid):
for other_id in sessions[uid]:
await peers[other_id][0].send('END_SESSION {}'.format(uid))
sessions[other_id].remove(uid)
print("Cleaned up {} -> {} session".format(uid, other_id))
del sessions[uid]
async def end_session(uid, other_id):
if not other_id in sessions[uid]:
return
if not uid in sessions[other_id]:
return
if not other_id in peers:
return
await peers[other_id][0].send('END_SESSION {}'.format(uid))
sessions[uid].remove(other_id)
sessions[other_id].remove(uid)
async def remove_peer(uid):
await cleanup_session(uid)
if uid in peers:
ws, raddr, status = peers[uid]
del peers[uid]
await ws.close()
print("Disconnected from peer {!r} at {!r}".format(uid, raddr))
if uid in producers:
for peer_id in listeners:
await peers[peer_id][0].send('REMOVE PRODUCER {}'.format(uid))
if uid in producers:
producers.remove(uid)
elif uid in consumers:
consumers.remove(uid)
elif uid in listeners:
listeners.remove(uid)
############### Handler functions ###############
async def connection_handler(ws, uid):
global peers, sessions
raddr = ws.remote_address
peers_status = []
peers[uid] = [ws, raddr, peers_status]
print("Registered peer {!r} at {!r}".format(uid, raddr))
while True:
# Receive command, wait forever if necessary
msg = await recv_msg_ping(ws, raddr)
# Update current status
peers_status = peers[uid][2]
# Requested a session with a specific peer
if msg.startswith('START_SESSION '):
print("{!r} command {!r}".format(uid, msg))
_, callee_id = msg.split(maxsplit=1)
if callee_id not in peers:
await ws.send('ERROR peer {!r} not found'.format(callee_id))
continue
wsc = peers[callee_id][0]
await wsc.send('START_SESSION {}'.format(uid))
print('Session from {!r} ({!r}) to {!r} ({!r})'
''.format(uid, raddr, callee_id, wsc.remote_address))
# Register session
peers[uid][2] = peer_status = 'session'
sessions[uid].append(callee_id)
peers[callee_id][2] = 'session'
sessions[callee_id] .append(uid)
elif msg.startswith('END_SESSION '):
_, peer_id = msg.split(maxsplit=1)
await end_session(uid, peer_id)
elif msg.startswith('LIST'):
answer = ' '.join(['LIST'] + [other_id for other_id in peers if other_id in producers])
await ws.send(answer)
# We are in a session, messages must be relayed
else:
# We're in a session, route message to connected peer
msg = json.loads(msg)
other_id = msg['peer-id']
try:
wso, oaddr, status = peers[other_id]
except KeyError:
continue
msg['peer-id'] = uid
msg = json.dumps(msg)
print("{} -> {}: {}".format(uid, other_id, msg))
await wso.send(msg)
async def register_peer(ws):
'''
Register peer
'''
raddr = ws.remote_address
msg = await ws.recv()
cmd, typ = msg.split(maxsplit=1)
uid = str(uuid.uuid4())
while uid in peers:
uid = str(uuid.uuid4())
if cmd != 'REGISTER':
await ws.close(code=1002, reason='invalid protocol')
raise Exception("Invalid registration from {!r}".format(raddr))
if typ not in ('PRODUCER', 'CONSUMER', 'LISTENER'):
await ws.close(code=1002, reason='invalid protocol')
raise Exception("Invalid registration from {!r}".format(raddr))
# Send back a HELLO
await ws.send('REGISTERED {}'.format(uid))
return typ, uid
async def handler(ws, path):
'''
All incoming messages are handled here. @path is unused.
'''
raddr = ws.remote_address
print("Connected to {!r}".format(raddr))
try:
typ, peer_id = await register_peer(ws)
except:
return
if typ == 'PRODUCER':
for other_id in listeners:
await peers[other_id][0].send('ADD PRODUCER {}'.format(peer_id))
producers.add(peer_id)
elif typ == 'CONSUMER':
consumers.add(peer_id)
elif typ == 'LISTENER':
listeners.add(peer_id)
try:
await connection_handler(ws, peer_id)
except websockets.ConnectionClosed:
print("Connection to peer {!r} closed, exiting handler".format(raddr))
finally:
await remove_peer(peer_id)
if options.disable_ssl:
sslctx = None
else:
# Create an SSL context to be used by the websocket server
certpath = '.'
chain_pem = os.path.join(certpath, 'cert.pem')
key_pem = os.path.join(certpath, 'key.pem')
sslctx = ssl.create_default_context()
try:
sslctx.load_cert_chain(chain_pem, keyfile=key_pem)
except FileNotFoundError:
print("Certificates not found, did you run generate_cert.sh?")
sys.exit(1)
# FIXME
sslctx.check_hostname = False
sslctx.verify_mode = ssl.CERT_NONE
print("Listening on wss://{}:{}".format(*ADDR_PORT))
# Websocket server
wsd = websockets.serve(handler, *ADDR_PORT, ssl=sslctx,
# Maximum number of messages that websockets will pop
# off the asyncio and OS buffers per connection. See:
# https://websockets.readthedocs.io/en/stable/api.html#websockets.protocol.WebSocketCommonProtocol
max_queue=16)
logger = logging.getLogger('websockets.server')
logger.setLevel(logging.ERROR)
logger.addHandler(logging.StreamHandler())
asyncio.get_event_loop().run_until_complete(wsd)
asyncio.get_event_loop().run_forever()

36
www/index.html Normal file
View file

@ -0,0 +1,36 @@
<!DOCTYPE html>
<!--
vim: set sts=2 sw=2 et :
Demo Javascript app for negotiating and streaming a sendrecv webrtc stream
with a GStreamer app. Runs only in passive mode, i.e., responds to offers
with answers, exchanges ICE candidates, and streams.
Author: Nirbheek Chauhan <nirbheek@centricular.com>
-->
<html>
<head>
<style>
.error { color: red; }
</style>
<link rel="stylesheet" type="text/css" href="theme.css">
<script src="https://webrtc.github.io/adapter/adapter-latest.js"></script>
<script src="webrtc.js"></script>
<script>window.onload = setup;</script>
</head>
<body>
<div class="holygrail-body">
<div class="content">
<div id="sessions">
</div>
<div id="image-holder">
<img id="image"></img>
</div>
</div>
<ul class="nav" id="camera-list">
</ul>
</div>
</body>
</html>

141
www/theme.css Normal file
View file

@ -0,0 +1,141 @@
/* Reset CSS from Eric Meyer */
html, body, div, span, applet, object, iframe,
h1, h2, h3, h4, h5, h6, p, blockquote, pre,
a, abbr, acronym, address, big, cite, code,
del, dfn, em, img, ins, kbd, q, s, samp,
small, strike, strong, sub, sup, tt, var,
b, u, i, center,
dl, dt, dd, ol, ul, li,
fieldset, form, label, legend,
table, caption, tbody, tfoot, thead, tr, th, td,
article, aside, canvas, details, embed,
figure, figcaption, footer, header, hgroup,
menu, nav, output, ruby, section, summary,
time, mark, audio, video {
margin: 0;
padding: 0;
border: 0;
font-size: 100%;
font: inherit;
vertical-align: baseline;
}
/* HTML5 display-role reset for older browsers */
article, aside, details, figcaption, figure,
footer, header, hgroup, menu, nav, section {
display: block;
}
body {
line-height: 1;
}
ol, ul {
list-style: none;
}
blockquote, q {
quotes: none;
}
blockquote:before, blockquote:after,
q:before, q:after {
content: '';
content: none;
}
table {
border-collapse: collapse;
border-spacing: 0;
}
/* Our style */
body{
display: flex;
flex-direction: column;
min-height: 100vh;
background-color: #222;
color: white;
}
.holygrail-body {
flex: 1 0 auto;
display: flex;
}
.holygrail-body .content {
width: 100%;
}
#sessions {
display: flex;
flex-direction: row;
flex-wrap: wrap;
justify-content: space-around;
}
.holygrail-body .nav {
width: 220px;
list-style: none;
text-align: left;
order: -1;
background-color: #333;
margin: 0;
}
@media (max-width: 700px) {
.holygrail-body {
flex-direction: column;
}
.holygrail-body .nav {
width: 100%;
}
}
.session p span {
float: right;
}
.session p {
padding-top: 5px;
padding-bottom: 5px;
}
.stream {
background-color: black;
width: 480px;
}
#camera-list {
text-align: center;
}
.button {
border: none;
padding: 8px;
text-align: center;
text-decoration: none;
display: inline-block;
font-size: 16px;
-webkit-transition-duration: 0.4s; /* Safari */
transition-duration: 0.4s;
cursor: pointer;
margin: 5px auto;
width: 90%;
}
.button1 {
background-color: #222;
color: white;
border: 2px solid #4CAF50;
word-wrap: anywhere;
}
.button1:hover {
background-color: #4CAF50;
color: white;
}
#image-holder {
display: flex;
flex-direction: row;
flex-wrap: wrap;
justify-content: space-around;
}

392
www/webrtc.js Normal file
View file

@ -0,0 +1,392 @@
/* vim: set sts=4 sw=4 et :
*
* Demo Javascript app for negotiating and streaming a sendrecv webrtc stream
* with a GStreamer app. Runs only in passive mode, i.e., responds to offers
* with answers, exchanges ICE candidates, and streams.
*
* Author: Nirbheek Chauhan <nirbheek@centricular.com>
*/
// Set this to override the automatic detection in websocketServerConnect()
var ws_server;
var ws_port;
// Override with your own STUN servers if you want
var rtc_configuration = {iceServers: [{urls: "stun:stun.l.google.com:19302"},
/* TODO: do not keep these static and in clear text in production,
* and instead use one of the mechanisms discussed in
* https://groups.google.com/forum/#!topic/discuss-webrtc/nn8b6UboqRA
*/
{'urls': 'turn:turn.homeneural.net:3478?transport=udp',
'credential': '1qaz2wsx',
'username': 'test'
}],
/* Uncomment the following line to ensure the turn server is used
* while testing. This should be kept commented out in production,
* as non-relay ice candidates should be preferred
*/
// iceTransportPolicy: "relay",
};
var sessions = {}
/* https://stackoverflow.com/questions/105034/create-guid-uuid-in-javascript */
function getOurId() {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8);
return v.toString(16);
});
}
function Uint8ToString(u8a){
var CHUNK_SZ = 0x8000;
var c = [];
for (var i=0; i < u8a.length; i+=CHUNK_SZ) {
c.push(String.fromCharCode.apply(null, u8a.subarray(i, i+CHUNK_SZ)));
}
return c.join("");
}
function Session(our_id, peer_id, closed_callback) {
this.peer_connection = null;
this.ws_conn = null;
this.peer_id = peer_id;
this.our_id = our_id;
this.closed_callback = closed_callback;
this.getVideoElement = function() {
return document.getElementById("stream-" + this.our_id);
};
this.resetState = function() {
if (this.peer_connection) {
this.peer_connection.close();
this.peer_connection = null;
}
var videoElement = this.getVideoElement();
if (videoElement) {
videoElement.pause();
videoElement.src = "";
}
var session_div = document.getElementById("session-" + this.our_id);
if (session_div) {
session_div.parentNode.removeChild(session_div);
}
if (this.ws_conn) {
this.ws_conn.close();
this.ws_conn = null;
}
};
this.handleIncomingError = function(error) {
this.resetState();
this.closed_callback(this.our_id);
};
this.setStatus = function(text) {
console.log(text);
var span = document.getElementById("status-" + this.our_id);
// Don't set the status if it already contains an error
if (!span.classList.contains('error'))
span.textContent = text;
};
this.setError = function(text) {
console.error(text);
console.log(this);
var span = document.getElementById("status-" + this.our_id);
span.textContent = text;
span.classList.add('error');
this.resetState();
this.closed_callback(this.our_id);
};
// Local description was set, send it to peer
this.onLocalDescription = function(desc) {
console.log("Got local description: " + JSON.stringify(desc), this);
var thiz = this;
this.peer_connection.setLocalDescription(desc).then(() => {
this.setStatus("Sending SDP answer");
var sdp = {'peer-id': this.peer_id, 'sdp': this.peer_connection.localDescription.toJSON()}
this.ws_conn.send(JSON.stringify(sdp));
}).catch(function(e) {
thiz.setError(e);
});
};
// SDP offer received from peer, set remote description and create an answer
this.onIncomingSDP = function(sdp) {
this.peer_connection.setRemoteDescription(sdp).then(() => {
this.setStatus("Remote SDP set");
if (sdp.type != "offer")
return;
this.setStatus("Got SDP offer");
this.peer_connection.createAnswer()
.then(this.onLocalDescription.bind(this)).catch(this.setError);
}).catch(this.setError);
};
// ICE candidate received from peer, add it to the peer connection
this.onIncomingICE = function(ice) {
var candidate = new RTCIceCandidate(ice);
this.peer_connection.addIceCandidate(candidate).catch(this.setError);
};
this.onServerMessage = function(event) {
console.log("Received " + event.data);
if (event.data.startsWith("REGISTERED")) {
this.setStatus("Registered with server");
this.connectPeer();
} else if (event.data.startsWith("ERROR")) {
this.handleIncomingError(event.data);
return;
} else {
// Handle incoming JSON SDP and ICE messages
try {
msg = JSON.parse(event.data);
} catch (e) {
if (e instanceof SyntaxError) {
this.handleIncomingError("Error parsing incoming JSON: " + event.data);
} else {
this.handleIncomingError("Unknown error parsing response: " + event.data);
}
return;
}
// Incoming JSON signals the beginning of a call
if (!this.peer_connection)
this.createCall(msg);
if (msg.sdp != null) {
this.onIncomingSDP(msg.sdp);
} else if (msg.ice != null) {
this.onIncomingICE(msg.ice);
} else {
this.handleIncomingError("Unknown incoming JSON: " + msg);
}
}
};
this.streamIsPlaying = function(e) {
this.setStatus("Streaming");
};
this.onServerClose = function(event) {
this.resetState();
this.closed_callback(this.our_id);
};
this.onServerError = function(event) {
this.handleIncomingError('Server error');
};
this.websocketServerConnect = function() {
// Clear errors in the status span
var span = document.getElementById("status-" + this.our_id);
span.classList.remove('error');
span.textContent = '';
console.log("Our ID:", this.our_id);
var ws_port = ws_port || '8443';
if (window.location.protocol.startsWith ("file")) {
var ws_server = ws_server || "127.0.0.1";
} else if (window.location.protocol.startsWith ("http")) {
var ws_server = ws_server || window.location.hostname;
} else {
throw new Error ("Don't know how to connect to the signalling server with uri" + window.location);
}
var ws_url = 'ws://' + ws_server + ':' + ws_port
this.setStatus("Connecting to server " + ws_url);
this.ws_conn = new WebSocket(ws_url);
/* When connected, immediately register with the server */
this.ws_conn.addEventListener('open', (event) => {
this.ws_conn.send('REGISTER CONSUMER');
this.setStatus("Registering with server");
});
this.ws_conn.addEventListener('error', this.onServerError.bind(this));
this.ws_conn.addEventListener('message', this.onServerMessage.bind(this));
this.ws_conn.addEventListener('close', this.onServerClose.bind(this));
};
this.connectPeer = function() {
this.setStatus("Connecting " + this.peer_id);
this.ws_conn.send("START_SESSION " + this.peer_id);
};
this.onRemoteStreamAdded = function(event) {
var videoTracks = event.stream.getVideoTracks();
var audioTracks = event.stream.getAudioTracks();
if (videoTracks.length > 0) {
console.log('Incoming stream: ' + videoTracks.length + ' video tracks and ' + audioTracks.length + ' audio tracks');
this.getVideoElement().srcObject = event.stream;
this.getVideoElement().play();
} else {
this.handleIncomingError('Stream with unknown tracks added, resetting');
}
};
this.createCall = function(msg) {
console.log('Creating RTCPeerConnection');
this.peer_connection = new RTCPeerConnection(rtc_configuration);
this.peer_connection.onaddstream = this.onRemoteStreamAdded.bind(this);
this.peer_connection.ondatachannel = (event) => {
console.log('Data channel created');
let receive_channel = event.channel;
let buffer = [];
receive_channel.onopen = (event) => {
console.log ("Receive channel opened");
}
receive_channel.onclose = (event) => {
console.log ("Receive channel closed");
}
receive_channel.onerror = (event) => {
console.log ("Error on receive channel", event.data);
}
receive_channel.onmessage = (event) => {
if (typeof event.data === 'string' || event.data instanceof String) {
if (event.data == 'BEGIN_IMAGE')
buffer = [];
else if (event.data == 'END_IMAGE') {
var decoder = new TextDecoder("ascii");
var array_buffer = new Uint8Array(buffer);
var str = decoder.decode(array_buffer);
let img = document.getElementById("image");
img.src = 'data:image/png;base64, ' + str;
}
} else {
var i, len = buffer.length
var view = new DataView(event.data);
for (i = 0; i < view.byteLength; i++) {
buffer[len + i] = view.getUint8(i);
}
}
}
}
if (!msg.sdp) {
console.log("WARNING: First message wasn't an SDP message!?");
}
this.peer_connection.onicecandidate = (event) => {
if (event.candidate == null) {
console.log("ICE Candidate was null, done");
return;
}
this.ws_conn.send(JSON.stringify({'ice': event.candidate.toJSON(), 'peer-id': this.peer_id}));
};
this.setStatus("Created peer connection for call, waiting for SDP");
};
document.getElementById("stream-" + this.our_id).addEventListener("playing", this.streamIsPlaying.bind(this), false);
this.websocketServerConnect();
}
function startSession() {
var peer_id = document.getElementById("camera-id").value;
if (peer_id === "") {
return;
}
sessions[peer_id] = new Session(peer_id);
}
function session_closed(peer_id) {
sessions[peer_id] = null;
}
function addPeer(peer_id) {
var nav_ul = document.getElementById("camera-list");
var li_str = '<li id="peer-' + peer_id + '"><button class="button button1">' + peer_id + '</button></li>'
nav_ul.insertAdjacentHTML('beforeend', li_str);
var li = document.getElementById("peer-" + peer_id);
li.onclick = function(e) {
var sessions_div = document.getElementById('sessions');
var our_id = getOurId();
var session_div_str = '<div class="session" id="session-' + our_id + '"><video preload="none" class="stream" id="stream-' + our_id + '"></video><p>Status: <span id="status-' + our_id + '">unknown</span></p></div>'
sessions_div.insertAdjacentHTML('beforeend', session_div_str);
sessions[peer_id] = new Session(our_id, peer_id, session_closed);
}
}
function clearPeers() {
var nav_ul = document.getElementById("camera-list");
while (nav_ul.firstChild) {
nav_ul.removeChild(nav_ul.firstChild);
}
}
function onServerMessage(event) {
console.log("Received " + event.data);
if (event.data.startsWith("REGISTERED ")) {
console.log("Listener registered");
ws_conn.send('LIST');
} else if (event.data.startsWith('LIST')) {
var split = event.data.split(' ');
clearPeers();
for (var i = 1; i < split.length; i++) {
addPeer(split[i]);
}
} else if (event.data.startsWith('ADD PRODUCER')) {
var split = event.data.split(' ');
addPeer(split[2]);
} else if (event.data.startsWith('REMOVE PRODUCER')) {
var split = event.data.split(' ');
var li = document.getElementById("peer-" + split[2]);
li.parentNode.removeChild(li);
}
};
function clearConnection() {
ws_conn.removeEventListener('error', onServerError);
ws_conn.removeEventListener('message', onServerMessage);
ws_conn.removeEventListener('close', onServerClose);
ws_conn = null;
}
function onServerClose(event) {
clearConnection();
clearPeers();
console.log("Close");
window.setTimeout(connect, 1000);
};
function onServerError(event) {
clearConnection();
clearPeers();
console.log("Error", event);
window.setTimeout(connect, 1000);
};
function connect() {
var ws_port = ws_port || '8443';
if (window.location.protocol.startsWith ("file")) {
var ws_server = ws_server || "127.0.0.1";
} else if (window.location.protocol.startsWith ("http")) {
var ws_server = ws_server || window.location.hostname;
} else {
throw new Error ("Don't know how to connect to the signalling server with uri" + window.location);
}
var ws_url = 'ws://' + ws_server + ':' + ws_port
console.log("Connecting listener");
ws_conn = new WebSocket(ws_url);
ws_conn.addEventListener('open', (event) => {
ws_conn.send('REGISTER LISTENER');
});
ws_conn.addEventListener('error', onServerError);
ws_conn.addEventListener('message', onServerMessage);
ws_conn.addEventListener('close', onServerClose);
}
function setup() {
connect();
}