mirror of
https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs.git
synced 2024-09-24 13:00:12 +00:00
Initial commit
This commit is contained in:
commit
79fb66f338
17 changed files with 5163 additions and 0 deletions
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
|||
/target
|
||||
*.dot
|
1672
Cargo.lock
generated
Normal file
1672
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
5
Cargo.toml
Normal file
5
Cargo.toml
Normal file
|
@ -0,0 +1,5 @@
|
|||
[workspace]
|
||||
|
||||
members = [
|
||||
"plugins",
|
||||
]
|
19
LICENSE
Normal file
19
LICENSE
Normal 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
120
README.md
Normal 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
51
plugins/Cargo.toml
Normal 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
3
plugins/build.rs
Normal file
|
@ -0,0 +1,3 @@
|
|||
fn main() {
|
||||
gst_plugin_version_helper::info()
|
||||
}
|
22
plugins/src/lib.rs
Normal file
22
plugins/src/lib.rs
Normal 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")
|
||||
);
|
393
plugins/src/signaller/imp.rs
Normal file
393
plugins/src/signaller/imp.rs
Normal 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!(),
|
||||
}
|
||||
}
|
||||
}
|
61
plugins/src/signaller/mod.rs
Normal file
61
plugins/src/signaller/mod.rs
Normal 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()
|
||||
}
|
||||
}
|
1568
plugins/src/webrtcsink/imp.rs
Normal file
1568
plugins/src/webrtcsink/imp.rs
Normal file
File diff suppressed because it is too large
Load diff
120
plugins/src/webrtcsink/mod.rs
Normal file
120
plugins/src/webrtcsink/mod.rs
Normal 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(),
|
||||
)
|
||||
}
|
318
plugins/src/webrtcsink/utils.rs
Normal file
318
plugins/src/webrtcsink/utils.rs
Normal 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
240
signalling/simple-server.py
Executable 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
36
www/index.html
Normal 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
141
www/theme.css
Normal 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
392
www/webrtc.js
Normal 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();
|
||||
}
|
Loading…
Reference in a new issue