From 9ded882ccbd580103de408c871fd0256f2ea2652 Mon Sep 17 00:00:00 2001 From: gaojiawei Date: Mon, 30 Oct 2023 20:31:23 +0800 Subject: [PATCH 01/12] gjw impl of videocall Signed-off-by: gaojiawei --- CMakeLists.txt | 1 + deps/libjuice | 2 +- examples/videocall/CMakeLists.txt | 26 +++ examples/videocall/dispatchqueue.cpp | 85 ++++++++++ examples/videocall/dispatchqueue.hpp | 51 ++++++ examples/videocall/helpers.cpp | 83 ++++++++++ examples/videocall/helpers.hpp | 59 +++++++ examples/videocall/main.cpp | 232 +++++++++++++++++++++++++++ 8 files changed, 538 insertions(+), 1 deletion(-) create mode 100644 examples/videocall/CMakeLists.txt create mode 100644 examples/videocall/dispatchqueue.cpp create mode 100644 examples/videocall/dispatchqueue.hpp create mode 100644 examples/videocall/helpers.cpp create mode 100644 examples/videocall/helpers.hpp create mode 100644 examples/videocall/main.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 4267d5a11..e2b74631c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -527,6 +527,7 @@ if(NOT NO_EXAMPLES) endif() if(NOT NO_MEDIA AND NOT NO_WEBSOCKET) add_subdirectory(examples/streamer) + add_subdirectory(examples/videocall) endif() add_subdirectory(examples/copy-paste) add_subdirectory(examples/copy-paste-capi) diff --git a/deps/libjuice b/deps/libjuice index 5f753cad4..7d7a66d43 160000 --- a/deps/libjuice +++ b/deps/libjuice @@ -1 +1 @@ -Subproject commit 5f753cad49059cea4eb492eb5c11a3bbb4dd6324 +Subproject commit 7d7a66d439b2e3e55e3f2494ff1176d527335674 diff --git a/examples/videocall/CMakeLists.txt b/examples/videocall/CMakeLists.txt new file mode 100644 index 000000000..376898d7d --- /dev/null +++ b/examples/videocall/CMakeLists.txt @@ -0,0 +1,26 @@ +cmake_minimum_required(VERSION 3.7) +if(POLICY CMP0079) + cmake_policy(SET CMP0079 NEW) +endif() + +set(VIDEOCALL_SOURCES + main.cpp + dispatchqueue.cpp + helpers.cpp +) + +if(CMAKE_SYSTEM_NAME STREQUAL "WindowsStore") + message(error, "not supported") +else() + add_executable(videocall ${VIDEOCALL_SOURCES}) +endif() + +set_target_properties(videocall PROPERTIES + CXX_STANDARD 17 + OUTPUT_NAME videocall) + +set_target_properties(videocall PROPERTIES + XCODE_ATTRIBUTE_PRODUCT_BUNDLE_IDENTIFIER com.github.paullouisageneau.libdatachannel.examples.videocall) + +find_package(Threads REQUIRED) +target_link_libraries(videocall LibDataChannel::LibDataChannel Threads::Threads nlohmann_json::nlohmann_json) diff --git a/examples/videocall/dispatchqueue.cpp b/examples/videocall/dispatchqueue.cpp new file mode 100644 index 000000000..46fff54e8 --- /dev/null +++ b/examples/videocall/dispatchqueue.cpp @@ -0,0 +1,85 @@ +/** + * libdatachannel streamer example + * Copyright (c) 2020 Filip Klembara (in2core) + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + + +#include "dispatchqueue.hpp" + +DispatchQueue::DispatchQueue(std::string name, size_t threadCount) : + name{std::move(name)}, threads(threadCount) { + for(size_t i = 0; i < threads.size(); i++) + { + threads[i] = std::thread(&DispatchQueue::dispatchThreadHandler, this); + } +} + +DispatchQueue::~DispatchQueue() { + // Signal to dispatch threads that it's time to wrap up + std::unique_lock lock(lockMutex); + quit = true; + lock.unlock(); + condition.notify_all(); + + // Wait for threads to finish before we exit + for(size_t i = 0; i < threads.size(); i++) + { + if(threads[i].joinable()) + { + threads[i].join(); + } + } +} + +void DispatchQueue::removePending() { + std::unique_lock lock(lockMutex); + queue = {}; +} + +void DispatchQueue::dispatch(const fp_t& op) { + std::unique_lock lock(lockMutex); + queue.push(op); + + // Manual unlocking is done before notifying, to avoid waking up + // the waiting thread only to block again (see notify_one for details) + lock.unlock(); + condition.notify_one(); +} + +void DispatchQueue::dispatch(fp_t&& op) { + std::unique_lock lock(lockMutex); + queue.push(std::move(op)); + + // Manual unlocking is done before notifying, to avoid waking up + // the waiting thread only to block again (see notify_one for details) + lock.unlock(); + condition.notify_one(); +} + +void DispatchQueue::dispatchThreadHandler(void) { + std::unique_lock lock(lockMutex); + do { + //Wait until we have data or a quit signal + condition.wait(lock, [this]{ + return (queue.size() || quit); + }); + + //after wait, we own the lock + if(!quit && queue.size()) + { + auto op = std::move(queue.front()); + queue.pop(); + + //unlock now that we're done messing with the queue + lock.unlock(); + + op(); + + lock.lock(); + } + } while (!quit); +} diff --git a/examples/videocall/dispatchqueue.hpp b/examples/videocall/dispatchqueue.hpp new file mode 100644 index 000000000..2ee487e1f --- /dev/null +++ b/examples/videocall/dispatchqueue.hpp @@ -0,0 +1,51 @@ +/** + * libdatachannel streamer example + * Copyright (c) 2020 Filip Klembara (in2core) + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +#ifndef dispatchqueue_hpp +#define dispatchqueue_hpp + +#include +#include +#include +#include +#include +#include + +class DispatchQueue { + typedef std::function fp_t; + +public: + DispatchQueue(std::string name, size_t threadCount = 1); + ~DispatchQueue(); + + // dispatch and copy + void dispatch(const fp_t& op); + // dispatch and move + void dispatch(fp_t&& op); + + void removePending(); + + // Deleted operations + DispatchQueue(const DispatchQueue& rhs) = delete; + DispatchQueue& operator=(const DispatchQueue& rhs) = delete; + DispatchQueue(DispatchQueue&& rhs) = delete; + DispatchQueue& operator=(DispatchQueue&& rhs) = delete; + +private: + std::string name; + std::mutex lockMutex; + std::vector threads; + std::queue queue; + std::condition_variable condition; + bool quit = false; + + void dispatchThreadHandler(void); +}; + +#endif /* dispatchqueue_hpp */ diff --git a/examples/videocall/helpers.cpp b/examples/videocall/helpers.cpp new file mode 100644 index 000000000..36c76113c --- /dev/null +++ b/examples/videocall/helpers.cpp @@ -0,0 +1,83 @@ +/** + * libdatachannel streamer example + * Copyright (c) 2020 Filip Klembara (in2core) + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +#include "helpers.hpp" + +#include + +#ifdef _MSC_VER +// taken from https://stackoverflow.com/questions/10905892/equivalent-of-gettimeday-for-windows +#include +#include // for struct timeval + +struct timezone { + int tz_minuteswest; + int tz_dsttime; +}; + +int gettimeofday(struct timeval *tv, struct timezone *tz) { + if (tv) { + FILETIME filetime; /* 64-bit value representing the number of 100-nanosecond intervals since + January 1, 1601 00:00 UTC */ + ULARGE_INTEGER x; + ULONGLONG usec; + static const ULONGLONG epoch_offset_us = + 11644473600000000ULL; /* microseconds betweeen Jan 1,1601 and Jan 1,1970 */ + +#if _WIN32_WINNT >= _WIN32_WINNT_WIN8 + GetSystemTimePreciseAsFileTime(&filetime); +#else + GetSystemTimeAsFileTime(&filetime); +#endif + x.LowPart = filetime.dwLowDateTime; + x.HighPart = filetime.dwHighDateTime; + usec = x.QuadPart / 10 - epoch_offset_us; + tv->tv_sec = time_t(usec / 1000000ULL); + tv->tv_usec = long(usec % 1000000ULL); + } + if (tz) { + TIME_ZONE_INFORMATION timezone; + GetTimeZoneInformation(&timezone); + tz->tz_minuteswest = timezone.Bias; + tz->tz_dsttime = 0; + } + return 0; +} +#else +#include +#endif + +using namespace std; +using namespace rtc; + +ClientTrackData::ClientTrackData(shared_ptr track, shared_ptr sender) { + this->track = track; + this->sender = sender; +} + +void Client::setState(State state) { + std::unique_lock lock(_mutex); + this->state = state; +} + +Client::State Client::getState() { + std::shared_lock lock(_mutex); + return state; +} + +ClientTrack::ClientTrack(string id, shared_ptr trackData) { + this->id = id; + this->trackData = trackData; +} + +uint64_t currentTimeInMicroSeconds() { + struct timeval time; + gettimeofday(&time, NULL); + return uint64_t(time.tv_sec) * 1000 * 1000 + time.tv_usec; +} diff --git a/examples/videocall/helpers.hpp b/examples/videocall/helpers.hpp new file mode 100644 index 000000000..212548fd6 --- /dev/null +++ b/examples/videocall/helpers.hpp @@ -0,0 +1,59 @@ +/** + * libdatachannel streamer example + * Copyright (c) 2020 Filip Klembara (in2core) + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +#ifndef helpers_hpp +#define helpers_hpp + +#include "rtc/rtc.hpp" + +#include + +struct ClientTrackData { + std::shared_ptr track; + std::shared_ptr sender; + + ClientTrackData(std::shared_ptr track, std::shared_ptr sender); +}; + +struct Client { + enum class State { + Waiting, + WaitingForVideo, + WaitingForAudio, + Ready + }; + const std::shared_ptr & peerConnection = _peerConnection; + Client(std::shared_ptr pc) { + _peerConnection = pc; + } + std::optional> video; + std::optional> audio; + std::optional> dataChannel; + + void setState(State state); + State getState(); + + uint32_t rtpStartTimestamp = 0; + +private: + std::shared_mutex _mutex; + State state = State::Waiting; + std::string id; + std::shared_ptr _peerConnection; +}; + +struct ClientTrack { + std::string id; + std::shared_ptr trackData; + ClientTrack(std::string id, std::shared_ptr trackData); +}; + +uint64_t currentTimeInMicroSeconds(); + +#endif /* helpers_hpp */ diff --git a/examples/videocall/main.cpp b/examples/videocall/main.cpp new file mode 100644 index 000000000..e1720afca --- /dev/null +++ b/examples/videocall/main.cpp @@ -0,0 +1,232 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "dispatchqueue.hpp" +#include "helpers.hpp" + +using namespace std::chrono_literals; +using json = nlohmann::json; + +template std::weak_ptr +make_weak_ptr(std::shared_ptr ptr) { + return ptr; +} + +static std::unordered_map> clients{}; + +auto threadPool = DispatchQueue("Main", 1); + +const std::string signaling_serverip = "127.0.0.1"; +const int signaling_serverport = 8000; + +static inline bool isDisconnectedState(const rtc::PeerConnection::State& state) { + return state == rtc::PeerConnection::State::Disconnected || + state == rtc::PeerConnection::State::Failed || + state == rtc::PeerConnection::State::Closed; +} + +std::shared_ptr createPeerConnection( + const std::string& id, + const rtc::Configuration &config, + std::weak_ptr wws) { + + // create and setup PeerConnection + + auto pc = std::make_shared(config); + auto client = std::make_shared(pc); + + pc->onStateChange( + [id](rtc::PeerConnection::State state) { + std::cout << "state: ,peer: " << state << id << std::endl; + + if (isDisconnectedState(state)) { + // remove disconnected client + threadPool.dispatch([id]() { + clients.erase(id); + }); + } + } + ); + + pc->onGatheringStateChange( + [wpc = make_weak_ptr(pc), id, wws](rtc::PeerConnection::GatheringState state) { + std::cout << "Gathering State: " << state << std::endl; + if (state == rtc::PeerConnection::GatheringState::Complete) { + if(auto pc = wpc.lock()) { + auto description = pc->localDescription(); + + json message = { + {"id", id}, + {"type", description->typeString()}, + {"sdp", std::string(description.value())} + }; + + // Gathering complete, send answer + if (auto ws = wws.lock()) { + ws->send(message.dump()); + } else { + std::cout << "owner ship to websocket is expired" << std::endl; + } + } else { + std::cout << "owner ship to peerconn is expired" << std::endl; + } + } + }); + + // TODO(Jiawei): add video and audio + + pc->setLocalDescription(); + + return client; +} + +void handleOffer( + const std::string& id, + const rtc::Configuration& config, + const std::shared_ptr ws) { + + // create peerconnection + clients.emplace(id, createPeerConnection(id, config, make_weak_ptr(ws))); +} + +void handleWSMsg( + const json message, + const rtc::Configuration config, + const std::shared_ptr ws) { + + // the id field indicates which peer we are connecting to + auto it = message.find("id"); + + if (it == message.end()) { + std::cout << "id field not found" << std::endl; + return; + } + + auto id = it->get(); + + it = message.find("type"); + + if (it == message.end()) { + std::cout << "type field not found" << std::endl; + return; + } + + auto type = it->get(); + + if (type == "offer") { + // TODO handle offer + handleOffer(id, config, ws); + + // create peer connection + + // gen local desc + + // gen answer + + // send answer + } else if (type == "answer") { + // TODO + } else if (type == "leave") { + // TODO + } else if (type == "userbusy") { + // TODO + } else if (type == "useroffline") { + // TODO + } else { + std::cout << "unknown message type: " << type << std::endl; + } +} + +int main(int argc, char **argv) { + std::cout << "hello world" << std::endl; + + if (argc < 2) { + std::cerr << "Client id must be specified" << std::endl; + return 1; + } + + auto config = rtc::Configuration(); + config.disableAutoNegotiation = true; + // not setting stun server for now + // auto stunServer = std::string("stun:stun.l.google.com:19302"); + // config.iceServers.emplace_back(stunServer); + + // parse the client id from the cmd line + auto peerid = std::string(argv[1]); + + std::cout << "Client id is: " << peerid << std::endl; + + const auto localid = std::string("gjw"); + + // open connection to the signal server using websockets + auto ws = std::make_shared(); + + // register all the handlers here for handling the websocket + ws->onOpen( + []() { + std::cout << "connected to the signal server via websocket" << std::endl; + } + ); + + ws->onClosed( + []() { + std::cout << "websocket closed" << std::endl; + } + ); + + ws->onError( + [](const std::string& error) { + std::cout << "failed to connect the signal server due to: " << error << std::endl; + } + ); + + ws->onMessage( + [&config, &ws](std::variant data) { + if (!std::holds_alternative(data)) + return; + + auto message = json::parse(std::get(data)); + + // dispatch the messge to the threadpool for handling + + threadPool.dispatch( + [message, config, ws]() { + handleWSMsg(message, config, ws); + } + ); + } + ); + + // initiate connection with the signaling server + const std::string url = "ws://" + + signaling_serverip + + ":" + + std::to_string(signaling_serverport) + + "/" + + "join/" + + localid; + + std::cout << "the signaling server url is: " << url << std::endl; + + // connect to the singaling server + ws->open(url); + + std::cout << "Waiting for signaling to be connected..." << std::endl; + + while (!ws->isOpen()) { + if (ws->isClosed()) + return 1; + std::this_thread::sleep_for(100ms); + } + + return 0; +} \ No newline at end of file From fdefba3a64064be7be16a5b66252bfcebb54a68f Mon Sep 17 00:00:00 2001 From: gaojiawei Date: Tue, 31 Oct 2023 02:24:12 +0800 Subject: [PATCH 02/12] implement the basic skeleton of the videocall app Signed-off-by: gaojiawei --- .../signaling-server-gjw.py | 157 ++++++++++++++++++ examples/videocall/main.cpp | 47 +++++- 2 files changed, 196 insertions(+), 8 deletions(-) create mode 100755 examples/signaling-server-python-gjw/signaling-server-gjw.py diff --git a/examples/signaling-server-python-gjw/signaling-server-gjw.py b/examples/signaling-server-python-gjw/signaling-server-gjw.py new file mode 100755 index 000000000..c8b2745dd --- /dev/null +++ b/examples/signaling-server-python-gjw/signaling-server-gjw.py @@ -0,0 +1,157 @@ +#!/usr/bin/env python +# +# Python signaling server example for libdatachannel +# Copyright (c) 2020 Paul-Louis Ageneau +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at https://mozilla.org/MPL/2.0/. + +import sys +import ssl +import json +import asyncio +import logging +import websockets + + +logger = logging.getLogger('websockets') +logger.setLevel(logging.DEBUG) +logger.addHandler(logging.StreamHandler(sys.stdout)) + +# bookkeeping the clients' ids and the websocket instance +clients = {} + +# bookkeeping the clients' ids and it's peer's id +peermap = {} + +# basic APIs: +# join/{clientid} +# indicates a client has login to the server and may want to +# connect to a peer later on, we don't handle Dos attach for now! +# +# offer/{peerid} +# indicates a client attempts to negotiate a connection to a peer +# reject if the peer is alread in connection or the peer hasn't joined +# +# TODO + +async def sendResponse(websocket, id, type): + message = {} + message["id"] = id + message["type"] = type + + print(f"Client id: {id}, type: {type}") + + data = json.dumps(message) + + await websocket.send(data) + +async def handle_websocket(websocket, path): + client_id = None + destination_id = None + try: + splitted = path.split('/') + splitted.pop(0) + + request = splitted.pop(0) + + client_id = None + + if request == "join": + client_id = splitted.pop(0) + else: + raise RuntimeError("Not implemented yet") + + if not client_id: + raise RuntimeError("Missing client ID") + + if client_id in clients: + raise RuntimeError("Duplicated request for join") + + clients[client_id] = websocket + + while True: + data = await websocket.recv() + print('Client {} << {}'.format(client_id, data)) + + message = json.loads(data) + + destination_id = message['id'] + destination_websocket = clients.get(destination_id) + + request = message['type'] + + if not destination_websocket: + print('Peer {} not found'.format(destination_id)) + await sendResponse(websocket, client_id, "useroffline") + continue + + # reject multiple request for peerconnection + if request == "offer" and (client_id in peermap or destination_id in peermap): + print('Client {} already in peerconnection'.format(client_id)) + await sendResponse(websocket, client_id, "userbusy") + continue + + if request == "leave": + print('Client {} requests to leave'.format(client_id)) + await sendResponse(destination_websocket, client_id, "leave") + break + + # map the peer id to this client if necessary + # offer/answer + peermap[client_id] = destination_id + + print("Sending message to {}".format(destination_id)) + + message['id'] = client_id + data = json.dumps(message) + + print('Client {} >> {}'.format(destination_id, data)) + + await destination_websocket.send(data) + + except Exception as e: + print(e) + + finally: + if client_id: + del clients[client_id] + + if client_id in peermap: + del peermap[client_id] + + if destination_id: + destination_websocket = clients.get(destination_id) + + if destination_websocket: + await sendResponse(websocket, client_id, "leave") + + if destination_id in peermap: + del peermap[destination_id] + + print('Client {} disconnected'.format(client_id)) + + +async def main(): + # Usage: ./server.py [[host:]port] [SSL certificate file] + endpoint_or_port = sys.argv[1] if len(sys.argv) > 1 else "8000" + ssl_cert = sys.argv[2] if len(sys.argv) > 2 else None + + endpoint = endpoint_or_port if ':' in endpoint_or_port else "127.0.0.1:" + endpoint_or_port + + if ssl_cert: + ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER) + ssl_context.load_cert_chain(ssl_cert) + else: + ssl_context = None + + print('Listening on {}'.format(endpoint)) + host, port = endpoint.rsplit(':', 1) + + server = await websockets.serve(handle_websocket, host, int(port), ssl=ssl_context) + await server.wait_closed() + + +if __name__ == '__main__': + asyncio.run(main()) diff --git a/examples/videocall/main.cpp b/examples/videocall/main.cpp index e1720afca..0d194bf1e 100644 --- a/examples/videocall/main.cpp +++ b/examples/videocall/main.cpp @@ -46,7 +46,7 @@ std::shared_ptr createPeerConnection( pc->onStateChange( [id](rtc::PeerConnection::State state) { - std::cout << "state: ,peer: " << state << id << std::endl; + std::cout << "state: " << state << ", " << "peer: " << id << std::endl; if (isDisconnectedState(state)) { // remove disconnected client @@ -83,6 +83,12 @@ std::shared_ptr createPeerConnection( }); // TODO(Jiawei): add video and audio + rtc::Description::Video media("video", rtc::Description::Direction::RecvOnly); + media.addH264Codec(96); + media.setBitrate( + 3000); // Request 3Mbps (Browsers do not encode more than 2.5MBps from a webcam) + + auto track = pc->addTrack(media); pc->setLocalDescription(); @@ -141,12 +147,13 @@ void handleWSMsg( // TODO } else if (type == "useroffline") { // TODO + std::cout << "connection failed due to peer is offline: " << type << std::endl; } else { std::cout << "unknown message type: " << type << std::endl; } } -int main(int argc, char **argv) { +int main(int argc, char **argv) try { std::cout << "hello world" << std::endl; if (argc < 2) { @@ -161,11 +168,9 @@ int main(int argc, char **argv) { // config.iceServers.emplace_back(stunServer); // parse the client id from the cmd line - auto peerid = std::string(argv[1]); - - std::cout << "Client id is: " << peerid << std::endl; + auto localid = std::string(argv[1]); - const auto localid = std::string("gjw"); + std::cout << "Client id is: " << localid << std::endl; // open connection to the signal server using websockets auto ws = std::make_shared(); @@ -220,13 +225,39 @@ int main(int argc, char **argv) { // connect to the singaling server ws->open(url); - std::cout << "Waiting for signaling to be connected..." << std::endl; + std::cout << "waiting for signaling to be connected..." << std::endl; while (!ws->isOpen()) { - if (ws->isClosed()) + if (ws->isClosed()) { + std::cerr << "Failed to connect to the signal server" << std::endl; return 1; + } std::this_thread::sleep_for(100ms); } + auto quit = false; + + while (!quit) { + std::string command; + std::cout << "Enter quit or q to exit" << std::endl; + std::cin >> command; + std::cin.ignore(); + + if (command == "quit" || command == "q") { + std::cout << "exiting" << std::endl; + quit = true; + } else if (command == "connect") { + std::string peerid; + std::cin >> peerid; + std::cout << "connecting to " << peerid << std::endl; + handleOffer(peerid, config, ws); + } + } + + std::cout << "Cleaning up..." << std::endl; + return 0; +} catch (const std::exception &e) { + std::cerr << "Error: " << e.what() << std::endl; + return 1; } \ No newline at end of file From 0369a957a76fe2d9c6a95930bfd74f161503548b Mon Sep 17 00:00:00 2001 From: gaojiawei Date: Fri, 3 Nov 2023 10:05:35 +0800 Subject: [PATCH 03/12] implement simple webrtc client using JS Signed-off-by: gaojiawei --- .gitignore | 2 +- examples/videocall/index.html | 68 ++++++++ examples/videocall/webrtc-client.js | 237 ++++++++++++++++++++++++++++ 3 files changed, 306 insertions(+), 1 deletion(-) create mode 100644 examples/videocall/index.html create mode 100644 examples/videocall/webrtc-client.js diff --git a/.gitignore b/.gitignore index 87d597129..517f6308a 100644 --- a/.gitignore +++ b/.gitignore @@ -12,4 +12,4 @@ compile_commands.json /tests .DS_Store .idea - +build-host/ diff --git a/examples/videocall/index.html b/examples/videocall/index.html new file mode 100644 index 000000000..467a2f8b2 --- /dev/null +++ b/examples/videocall/index.html @@ -0,0 +1,68 @@ + + + + + libdatachannel media example + + + + +

libdatachannel streamer example client

+ +

Options

+
+ + +
+ +

+ + + +

State

+

ICE Connection state:

+

ICE Gathering state:

+

Signaling state:

+ + + +

Data Channel

+

+
+

SDP

+ +

Offer

+

+
+

Answer

+

+
+
+
+
diff --git a/examples/videocall/webrtc-client.js b/examples/videocall/webrtc-client.js
new file mode 100644
index 000000000..0ff942d56
--- /dev/null
+++ b/examples/videocall/webrtc-client.js
@@ -0,0 +1,237 @@
+const iceConnectionLog = document.getElementById('ice-connection-state'),
+    iceGatheringLog = document.getElementById('ice-gathering-state'),
+    signalingLog = document.getElementById('signaling-state'),
+    dataChannelLog = document.getElementById('data-channel');
+
+const clientId = randomId(10);
+const websocket = new WebSocket('ws://127.0.0.1:8000/' + 'join' + clientId);
+
+const remotePeer = null;
+
+websocket.onopen = () => {
+    document.getElementById('start').disabled = false;
+}
+
+websocket.onmessage = async (evt) => {
+    if (typeof evt.data !== 'string') {
+        return;
+    }
+    const message = JSON.parse(evt.data);
+    if (message.type == "offer") {
+        document.getElementById('offer-sdp').textContent = message.sdp;
+        await handleOffer(message)
+    }
+}
+
+let pc = null;
+let dc = null;
+
+function createPeerConnection() {
+    const config = {
+        bundlePolicy: "max-bundle",
+    };
+
+    if (document.getElementById('use-stun').checked) {
+        config.iceServers = [{urls: ['stun:stun.l.google.com:19302']}];
+    }
+
+    let pc = new RTCPeerConnection(config);
+
+    // Register some listeners to help debugging
+    pc.addEventListener('iceconnectionstatechange', () =>
+        iceConnectionLog.textContent += ' -> ' + pc.iceConnectionState);
+    iceConnectionLog.textContent = pc.iceConnectionState;
+
+    pc.addEventListener('icegatheringstatechange', () =>
+        iceGatheringLog.textContent += ' -> ' + pc.iceGatheringState);
+    iceGatheringLog.textContent = pc.iceGatheringState;
+
+    pc.addEventListener('signalingstatechange', () =>
+        signalingLog.textContent += ' -> ' + pc.signalingState);
+    signalingLog.textContent = pc.signalingState;
+
+    // Receive audio/video track
+    pc.ontrack = (evt) => {
+        document.getElementById('media').style.display = 'block';
+        const video = document.getElementById('video');
+        // always overrite the last stream - you may want to do something more clever in practice
+        video.srcObject = evt.streams[0]; // The stream groups audio and video tracks
+        video.play();
+    };
+
+    // Receive data channel
+    pc.ondatachannel = (evt) => {
+        dc = evt.channel;
+
+        dc.onopen = () => {
+            dataChannelLog.textContent += '- open\n';
+            dataChannelLog.scrollTop = dataChannelLog.scrollHeight;
+        };
+
+        let dcTimeout = null;
+        dc.onmessage = (evt) => {
+            if (typeof evt.data !== 'string') {
+                return;
+            }
+
+            dataChannelLog.textContent += '< ' + evt.data + '\n';
+            dataChannelLog.scrollTop = dataChannelLog.scrollHeight;
+
+            dcTimeout = setTimeout(() => {
+                if (!dc) {
+                    return;
+                }
+                const message = `Pong ${currentTimestamp()}`;
+                dataChannelLog.textContent += '> ' + message + '\n';
+                dataChannelLog.scrollTop = dataChannelLog.scrollHeight;
+                dc.send(message);
+            }, 1000);
+        }
+
+        dc.onclose = () => {
+            clearTimeout(dcTimeout);
+            dcTimeout = null;
+            dataChannelLog.textContent += '- close\n';
+            dataChannelLog.scrollTop = dataChannelLog.scrollHeight;
+        };
+    }
+
+    return pc;
+}
+
+async function waitGatheringComplete() {
+    return new Promise((resolve) => {
+        if (pc.iceGatheringState === 'complete') {
+            resolve();
+        } else {
+            pc.addEventListener('icegatheringstatechange', () => {
+                if (pc.iceGatheringState === 'complete') {
+                    resolve();
+                }
+            });
+        }
+    });
+}
+
+async function sendAnswer(pc) {
+    await pc.setLocalDescription(await pc.createAnswer());
+    await waitGatheringComplete();
+
+    const answer = pc.localDescription;
+    document.getElementById('answer-sdp').textContent = answer.sdp;
+
+    websocket.send(JSON.stringify({
+        id: "server",
+        type: answer.type,
+        sdp: answer.sdp,
+    }));
+}
+
+async function handleOffer(offer) {
+    pc = createPeerConnection();
+    await pc.setRemoteDescription(offer);
+    await sendAnswer(pc);
+}
+
+function sendRequest() {
+    // websocket.send(JSON.stringify({
+    //     id: "server",
+    //     type: "request",
+    // }));
+
+    // we should get generate our local sdp and send it to the remote end
+}
+
+async function getMedia(constraints) {
+    let stream = null;
+
+    try {
+      stream = await navigator.mediaDevices.getUserMedia(constraints);
+      /* use the stream */
+      console.log("Got user media");
+
+      const myVideo = document.getElementById('video-me');
+
+      myVideo.srcObject = stream;
+      myVideo.play()
+
+    } catch (err) {
+      /* handle the error */
+      console.log(err);
+    }
+}
+
+function start() {
+    peerID = document.getElementById('peerID').value;
+
+    if (!peerID) {
+        alert("Please input peerID before calling");
+        return;
+    }
+
+    document.getElementById('start').style.display = 'none';
+    document.getElementById('stop').style.display = 'inline-block';
+    document.getElementById('media').style.display = 'block';
+
+    sendRequest();
+
+    const constraints = {
+        video : true,
+        audio : true
+    }
+
+    // get user media for displaying the camera
+    getMedia(constraints);
+}
+
+function stop() {
+    document.getElementById('stop').style.display = 'none';
+    document.getElementById('media').style.display = 'none';
+    document.getElementById('start').style.display = 'inline-block';
+
+    // close data channel
+    if (dc) {
+        dc.close();
+        dc = null;
+    }
+
+    // close transceivers
+    if (pc.getTransceivers) {
+        pc.getTransceivers().forEach((transceiver) => {
+            if (transceiver.stop) {
+                transceiver.stop();
+            }
+        });
+    }
+
+    // close local audio/video
+    pc.getSenders().forEach((sender) => {
+        const track = sender.track;
+        if (track !== null) {
+            sender.track.stop();
+        }
+    });
+
+    // close peer connection
+    pc.close();
+    pc = null;
+}
+
+// Helper function to generate a random ID
+function randomId(length) {
+  const characters = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
+  const pickRandom = () => characters.charAt(Math.floor(Math.random() * characters.length));
+  return [...Array(length) ].map(pickRandom).join('');
+}
+
+// Helper function to generate a timestamp
+let startTime = null;
+function currentTimestamp() {
+    if (startTime === null) {
+        startTime = Date.now();
+        return 0;
+    } else {
+        return Date.now() - startTime;
+    }
+}
+

From bbbbf6486851cc09ea442ca71a3506f15be4ddd7 Mon Sep 17 00:00:00 2001
From: gaojiawei 
Date: Fri, 3 Nov 2023 14:59:11 +0800
Subject: [PATCH 04/12] a seems working webrtc videocall client

Signed-off-by: gaojiawei 
---
 .../signaling-server-gjw.py                   |  13 +-
 examples/videocall/index.html                 |   2 +
 examples/videocall/webrtc-client.js           | 154 +++++++++++++-----
 3 files changed, 125 insertions(+), 44 deletions(-)

diff --git a/examples/signaling-server-python-gjw/signaling-server-gjw.py b/examples/signaling-server-python-gjw/signaling-server-gjw.py
index c8b2745dd..3588c8528 100755
--- a/examples/signaling-server-python-gjw/signaling-server-gjw.py
+++ b/examples/signaling-server-python-gjw/signaling-server-gjw.py
@@ -1,4 +1,4 @@
-#!/usr/bin/env python
+#!/usr/bin/env python3
 #
 # Python signaling server example for libdatachannel
 # Copyright (c) 2020 Paul-Louis Ageneau
@@ -34,7 +34,6 @@
 # indicates a client attempts to negotiate a connection to a peer
 # reject if the peer is alread in connection or the peer hasn't joined
 #
-# TODO
 
 async def sendResponse(websocket, id, type):
     message = {}
@@ -73,7 +72,7 @@ async def handle_websocket(websocket, path):
 
         while True:
             data = await websocket.recv()
-            print('Client {} << {}'.format(client_id, data))
+            print('From Client {} >> {}'.format(client_id, data))
 
             message = json.loads(data)
 
@@ -93,9 +92,9 @@ async def handle_websocket(websocket, path):
                 await sendResponse(websocket, client_id, "userbusy")
                 continue
 
-            if request == "leave":
+            if request == "bye":
                 print('Client {} requests to leave'.format(client_id))
-                await sendResponse(destination_websocket, client_id, "leave")
+                await sendResponse(destination_websocket, client_id, "bye")
                 break
 
             # map the peer id to this client if necessary
@@ -107,7 +106,7 @@ async def handle_websocket(websocket, path):
             message['id'] = client_id
             data = json.dumps(message)
 
-            print('Client {} >> {}'.format(destination_id, data))
+            print('To Client {} << {}'.format(destination_id, data))
 
             await destination_websocket.send(data)
 
@@ -125,7 +124,7 @@ async def handle_websocket(websocket, path):
             destination_websocket = clients.get(destination_id)
 
             if destination_websocket:
-                await sendResponse(websocket, client_id, "leave")
+                await sendResponse(websocket, client_id, "bye")
 
             if destination_id in peermap:
                 del peermap[destination_id]
diff --git a/examples/videocall/index.html b/examples/videocall/index.html
index 467a2f8b2..abbd37ca0 100644
--- a/examples/videocall/index.html
+++ b/examples/videocall/index.html
@@ -30,6 +30,8 @@
 
 

libdatachannel streamer example client

+

haha

+

Options

diff --git a/examples/videocall/webrtc-client.js b/examples/videocall/webrtc-client.js index 0ff942d56..b0eb50891 100644 --- a/examples/videocall/webrtc-client.js +++ b/examples/videocall/webrtc-client.js @@ -4,31 +4,84 @@ const iceConnectionLog = document.getElementById('ice-connection-state'), dataChannelLog = document.getElementById('data-channel'); const clientId = randomId(10); -const websocket = new WebSocket('ws://127.0.0.1:8000/' + 'join' + clientId); +document.getElementById("my-id").innerHTML = clientId; + +const websocket = new WebSocket('ws://127.0.0.1:8000/' + 'join/' + clientId); const remotePeer = null; +let localstream = null; + websocket.onopen = () => { document.getElementById('start').disabled = false; } +let pc = null; +let dc = null; + +async function hangup() { + if (pc) { + pc.close();; + pc = null; + } + + // localStream.getTracks().forEach(track => track.stop()); + // localStream = null; + // startButton.disabled = false; + // hangupButton.disabled = true; + + // TODO stop track + // TODO show end session +} + +function handleSignalingMsg(message) { + + const peerId = message.id; + + switch (message.type) { + case 'offer': + handleOffer({type : message.type, sdp : message.sdp}, peerId); + break; + case 'answer': + handleAnswer({type : message.type, sdp : message.sdp}, peerId); + break; + case 'candidate': + handleCandidate(message); + break; + case 'ready': + // A second tab joined. This tab will initiate a call unless in a call already. + // if (pc) { + // console.log('already in call, ignoring'); + // return; + // } + // makeCall(); + // break; + case 'bye': + case 'useroffline': + case 'userbusy': + if (pc) { + hangup(); + } + break; + default: + console.log('unhandled', e); + break; + } +} + websocket.onmessage = async (evt) => { if (typeof evt.data !== 'string') { return; } const message = JSON.parse(evt.data); - if (message.type == "offer") { - document.getElementById('offer-sdp').textContent = message.sdp; - await handleOffer(message) - } -} -let pc = null; -let dc = null; -function createPeerConnection() { + handleSignalingMsg(message); +} + +async function createPeerConnection() { const config = { - bundlePolicy: "max-bundle", + // bundlePolicy: "max-bundle", }; if (document.getElementById('use-stun').checked) { @@ -53,12 +106,14 @@ function createPeerConnection() { // Receive audio/video track pc.ontrack = (evt) => { document.getElementById('media').style.display = 'block'; - const video = document.getElementById('video'); + const peervideo = document.getElementById('video-peer'); // always overrite the last stream - you may want to do something more clever in practice - video.srcObject = evt.streams[0]; // The stream groups audio and video tracks - video.play(); + peervideo.srcObject = evt.streams[0]; // The stream groups audio and video tracks + peervideo.play(); }; + localstream.getTracks().forEach(track => pc.addTrack(track, localstream)); + // Receive data channel pc.ondatachannel = (evt) => { dc = evt.channel; @@ -113,7 +168,7 @@ async function waitGatheringComplete() { }); } -async function sendAnswer(pc) { +async function sendAnswer(pc, peerId) { await pc.setLocalDescription(await pc.createAnswer()); await waitGatheringComplete(); @@ -121,38 +176,63 @@ async function sendAnswer(pc) { document.getElementById('answer-sdp').textContent = answer.sdp; websocket.send(JSON.stringify({ - id: "server", + id: peerId, type: answer.type, sdp: answer.sdp, })); } -async function handleOffer(offer) { - pc = createPeerConnection(); +async function handleOffer(offer, peerId) { + if (pc) { + console.error('existing peerconnection'); + return; + } + + pc = await createPeerConnection(); + document.getElementById('offer-sdp').textContent = offer.sdp; + await pc.setRemoteDescription(offer); - await sendAnswer(pc); + await sendAnswer(pc, peerId); } -function sendRequest() { - // websocket.send(JSON.stringify({ - // id: "server", - // type: "request", - // })); +async function handleAnswer(answer, peerId) { + if (!pc) { + console.log("No existing peerconn!"); + return; + } + + await pc.setRemoteDescription(answer); +} + +async function sendRequest() { + if (!peerID) { + console.log("Failed to send videocall request, null peerID"); + } + + pc = await createPeerConnection(); + + myOffer = await pc.createOffer(); // we should get generate our local sdp and send it to the remote end + websocket.send(JSON.stringify({ + id : peerID, + type : "offer", + sdp : myOffer.sdp + })); + + pc.setLocalDescription(myOffer); } async function getMedia(constraints) { - let stream = null; - try { - stream = await navigator.mediaDevices.getUserMedia(constraints); + localstream = await navigator.mediaDevices.getUserMedia(constraints); /* use the stream */ console.log("Got user media"); + document.getElementById('media').style.display = 'block'; const myVideo = document.getElementById('video-me'); - myVideo.srcObject = stream; + myVideo.srcObject = localstream; myVideo.play() } catch (err) { @@ -161,7 +241,7 @@ async function getMedia(constraints) { } } -function start() { +async function start() { peerID = document.getElementById('peerID').value; if (!peerID) { @@ -171,17 +251,10 @@ function start() { document.getElementById('start').style.display = 'none'; document.getElementById('stop').style.display = 'inline-block'; - document.getElementById('media').style.display = 'block'; - sendRequest(); + await sendRequest(); - const constraints = { - video : true, - audio : true - } - - // get user media for displaying the camera - getMedia(constraints); + console.log("3"); } function stop() { @@ -235,3 +308,10 @@ function currentTimestamp() { } } +const constraints = { + video : true, + audio : true +} + +getMedia(constraints); + From 01c56c6af4cb52dc6a705278d588bf706a58e361 Mon Sep 17 00:00:00 2001 From: gaojiawei Date: Mon, 13 Nov 2023 09:01:38 +0800 Subject: [PATCH 05/12] change signaling server API Signed-off-by: gaojiawei --- examples/videocall/webrtc-client.js | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/examples/videocall/webrtc-client.js b/examples/videocall/webrtc-client.js index b0eb50891..4041c08ed 100644 --- a/examples/videocall/webrtc-client.js +++ b/examples/videocall/webrtc-client.js @@ -6,7 +6,7 @@ const iceConnectionLog = document.getElementById('ice-connection-state'), const clientId = randomId(10); document.getElementById("my-id").innerHTML = clientId; -const websocket = new WebSocket('ws://127.0.0.1:8000/' + 'join/' + clientId); +const websocket = new WebSocket('ws://10.196.28.10:8888/' + 'join/' + clientId); const remotePeer = null; @@ -108,8 +108,11 @@ async function createPeerConnection() { document.getElementById('media').style.display = 'block'; const peervideo = document.getElementById('video-peer'); // always overrite the last stream - you may want to do something more clever in practice - peervideo.srcObject = evt.streams[0]; // The stream groups audio and video tracks - peervideo.play(); + + if (!peervideo.srcObject) { + peervideo.srcObject = evt.streams[0]; // The stream groups audio and video tracks + peervideo.play(); + } }; localstream.getTracks().forEach(track => pc.addTrack(track, localstream)); @@ -202,6 +205,7 @@ async function handleAnswer(answer, peerId) { } await pc.setRemoteDescription(answer); + console.log("set remote desc sdp done"); } async function sendRequest() { From df3d1b4082e43418ad725fde771fe04fe8b06ff7 Mon Sep 17 00:00:00 2001 From: gaojiawei Date: Mon, 13 Nov 2023 16:22:11 +0800 Subject: [PATCH 06/12] improve add streams Signed-off-by: gaojiawei --- examples/videocall/index.html | 2 +- examples/videocall/webrtc-client.js | 21 +++++++++++++++------ 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/examples/videocall/index.html b/examples/videocall/index.html index abbd37ca0..b165be302 100644 --- a/examples/videocall/index.html +++ b/examples/videocall/index.html @@ -51,7 +51,7 @@

State

Remote Peer

Me

- +

Data Channel

diff --git a/examples/videocall/webrtc-client.js b/examples/videocall/webrtc-client.js index 4041c08ed..deb63cddf 100644 --- a/examples/videocall/webrtc-client.js +++ b/examples/videocall/webrtc-client.js @@ -79,6 +79,8 @@ websocket.onmessage = async (evt) => { handleSignalingMsg(message); } +let inboundStream = null; + async function createPeerConnection() { const config = { // bundlePolicy: "max-bundle", @@ -103,19 +105,24 @@ async function createPeerConnection() { signalingLog.textContent += ' -> ' + pc.signalingState); signalingLog.textContent = pc.signalingState; + const peervideo = document.getElementById('video-peer'); + // Receive audio/video track pc.ontrack = (evt) => { - document.getElementById('media').style.display = 'block'; - const peervideo = document.getElementById('video-peer'); // always overrite the last stream - you may want to do something more clever in practice + if (evt.streams && evt.streams[0]) { + peervideo.srcObject = evt.streams[0]; + } else { + if (!inboundStream) { + inboundStream = new MediaStream(); + peervideo.srcObject = inboundStream; + } - if (!peervideo.srcObject) { - peervideo.srcObject = evt.streams[0]; // The stream groups audio and video tracks - peervideo.play(); + inboundStream.addTrack(evt.track); } }; - localstream.getTracks().forEach(track => pc.addTrack(track, localstream)); + localstream.getTracks().forEach(track => pc.addTrack(track)); // Receive data channel pc.ondatachannel = (evt) => { @@ -206,6 +213,7 @@ async function handleAnswer(answer, peerId) { await pc.setRemoteDescription(answer); console.log("set remote desc sdp done"); + document.getElementById('answer-sdp').textContent = answer.sdp; } async function sendRequest() { @@ -225,6 +233,7 @@ async function sendRequest() { })); pc.setLocalDescription(myOffer); + document.getElementById('offer-sdp').textContent = myOffer.sdp; } async function getMedia(constraints) { From 8aeb93fff7d83d25728faf08096917f56e98bc89 Mon Sep 17 00:00:00 2001 From: gaojiawei Date: Mon, 13 Nov 2023 18:06:56 +0800 Subject: [PATCH 07/12] add candiate support Signed-off-by: gaojiawei --- examples/videocall/webrtc-client.js | 46 ++++++++++++++++++++++++----- 1 file changed, 39 insertions(+), 7 deletions(-) diff --git a/examples/videocall/webrtc-client.js b/examples/videocall/webrtc-client.js index deb63cddf..5dfc4fc35 100644 --- a/examples/videocall/webrtc-client.js +++ b/examples/videocall/webrtc-client.js @@ -40,13 +40,19 @@ function handleSignalingMsg(message) { switch (message.type) { case 'offer': + console.log("start handling offer"); handleOffer({type : message.type, sdp : message.sdp}, peerId); + console.log("end handling offer"); break; case 'answer': + console.log("start handling answer"); handleAnswer({type : message.type, sdp : message.sdp}, peerId); + console.log("end handling answer"); break; case 'candidate': - handleCandidate(message); + console.log("start handling candidate"); + handleCandidate({type : message.type, candidate : message.candidate}, peerId); + console.log("end handling candidate"); break; case 'ready': // A second tab joined. This tab will initiate a call unless in a call already. @@ -69,7 +75,7 @@ function handleSignalingMsg(message) { } } -websocket.onmessage = async (evt) => { +websocket.onmessage = (evt) => { if (typeof evt.data !== 'string') { return; } @@ -81,7 +87,7 @@ websocket.onmessage = async (evt) => { let inboundStream = null; -async function createPeerConnection() { +function createPeerConnection(peerId) { const config = { // bundlePolicy: "max-bundle", }; @@ -105,6 +111,23 @@ async function createPeerConnection() { signalingLog.textContent += ' -> ' + pc.signalingState); signalingLog.textContent = pc.signalingState; + pc.addEventListener('icecandidate', (event) => { + if (event.candidate !== null) { + console.log("Sending candidate to peer: ", peerId); + console.log(event.candidate); + + websocket.send(JSON.stringify({ + id: peerId, + type: 'candidate', + candidate: event.candidate, + })); + + } else { + /* there are no more candidates coming during this negotiation */ + console.log("No candidates found!") + } + }); + const peervideo = document.getElementById('video-peer'); // Receive audio/video track @@ -198,7 +221,7 @@ async function handleOffer(offer, peerId) { return; } - pc = await createPeerConnection(); + pc = createPeerConnection(peerId); document.getElementById('offer-sdp').textContent = offer.sdp; await pc.setRemoteDescription(offer); @@ -211,17 +234,26 @@ async function handleAnswer(answer, peerId) { return; } - await pc.setRemoteDescription(answer); + pc.setRemoteDescription(answer); console.log("set remote desc sdp done"); document.getElementById('answer-sdp').textContent = answer.sdp; } +async function handleCandidate(candidate, peerId) { + if (!pc) { + console.log("No existing peerconn!"); + return; + } + + pc.addIceCandidate(new RTCIceCandidate(candidate.candidate)); +} + async function sendRequest() { if (!peerID) { console.log("Failed to send videocall request, null peerID"); } - pc = await createPeerConnection(); + pc = createPeerConnection(peerID); myOffer = await pc.createOffer(); @@ -232,7 +264,7 @@ async function sendRequest() { sdp : myOffer.sdp })); - pc.setLocalDescription(myOffer); + await pc.setLocalDescription(myOffer); document.getElementById('offer-sdp').textContent = myOffer.sdp; } From dfdf4b8fff9ae1348bb866285c5b15d2ec08bf7c Mon Sep 17 00:00:00 2001 From: gaojiawei Date: Mon, 13 Nov 2023 20:57:19 +0800 Subject: [PATCH 08/12] add candidates after remote description setup Signed-off-by: gaojiawei --- examples/videocall/app.py | 22 ++++++++++++++++++ .../videocall/{ => static}/webrtc-client.js | 23 ++++++++++++++++--- examples/videocall/{ => templates}/index.html | 0 3 files changed, 42 insertions(+), 3 deletions(-) create mode 100644 examples/videocall/app.py rename examples/videocall/{ => static}/webrtc-client.js (93%) rename examples/videocall/{ => templates}/index.html (100%) diff --git a/examples/videocall/app.py b/examples/videocall/app.py new file mode 100644 index 000000000..1d4e69426 --- /dev/null +++ b/examples/videocall/app.py @@ -0,0 +1,22 @@ +from flask import Flask, render_template +import sys + +app = Flask(__name__, static_url_path='', static_folder='static') + +@app.route('/') +def root(): + return render_template('index.html') + + +if __name__ == '__main__': + + address = '127.0.0.1' + port = '5566' + + if len(sys.argv) == 3: + address = sys.argv[1] + port = sys.argv[2] + + print("server running at ", address, port) + + app.run(debug=True, host=address, port=port) \ No newline at end of file diff --git a/examples/videocall/webrtc-client.js b/examples/videocall/static/webrtc-client.js similarity index 93% rename from examples/videocall/webrtc-client.js rename to examples/videocall/static/webrtc-client.js index 5dfc4fc35..7fbb8fbfe 100644 --- a/examples/videocall/webrtc-client.js +++ b/examples/videocall/static/webrtc-client.js @@ -89,11 +89,12 @@ let inboundStream = null; function createPeerConnection(peerId) { const config = { - // bundlePolicy: "max-bundle", + bundlePolicy: "max-bundle", }; if (document.getElementById('use-stun').checked) { config.iceServers = [{urls: ['stun:stun.l.google.com:19302']}]; + config.iceTransportPolicy = "all"; } let pc = new RTCPeerConnection(config); @@ -228,15 +229,23 @@ async function handleOffer(offer, peerId) { await sendAnswer(pc, peerId); } +let candidates = []; + async function handleAnswer(answer, peerId) { if (!pc) { console.log("No existing peerconn!"); return; } - pc.setRemoteDescription(answer); + await pc.setRemoteDescription(answer); console.log("set remote desc sdp done"); document.getElementById('answer-sdp').textContent = answer.sdp; + + // After successfully received all answers, we check if any candidates + // need to be added! + + candidates.forEach((c) => pc.addIceCandidate(c)); + candidates = []; } async function handleCandidate(candidate, peerId) { @@ -245,7 +254,15 @@ async function handleCandidate(candidate, peerId) { return; } - pc.addIceCandidate(new RTCIceCandidate(candidate.candidate)); + // there might be a chance remote description hasn't been set yet! + // in that case we delay adding candidates! + if (!pc.setRemoteDescription) { + candidates.push(new RTCIceCandidate(candidate.candidate)); + } else { + candidates.forEach((c) => pc.addIceCandidate(c)); + candidates = []; + } + } async function sendRequest() { diff --git a/examples/videocall/index.html b/examples/videocall/templates/index.html similarity index 100% rename from examples/videocall/index.html rename to examples/videocall/templates/index.html From 060ef61fa8dc583c27f64f7d4ecf3f43c0a6d914 Mon Sep 17 00:00:00 2001 From: gaojiawei Date: Mon, 13 Nov 2023 21:46:55 +0800 Subject: [PATCH 09/12] modify title Signed-off-by: gaojiawei --- examples/videocall/templates/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/videocall/templates/index.html b/examples/videocall/templates/index.html index b165be302..4bb936066 100644 --- a/examples/videocall/templates/index.html +++ b/examples/videocall/templates/index.html @@ -28,7 +28,7 @@ -

libdatachannel streamer example client

+

Mi VideoChat example client

haha

From 97bec32ecee21229dc898d48bb836f590e32f8ac Mon Sep 17 00:00:00 2001 From: gaojiawei Date: Wed, 15 Nov 2023 22:13:09 +0800 Subject: [PATCH 10/12] implement signaling part of videocall client not finished Signed-off-by: gaojiawei --- examples/videocall/main.cpp | 67 ++++++++++++++++++++++++++----------- 1 file changed, 48 insertions(+), 19 deletions(-) diff --git a/examples/videocall/main.cpp b/examples/videocall/main.cpp index 0d194bf1e..498366692 100644 --- a/examples/videocall/main.cpp +++ b/examples/videocall/main.cpp @@ -21,12 +21,12 @@ make_weak_ptr(std::shared_ptr ptr) { return ptr; } -static std::unordered_map> clients{}; +static std::unordered_map> peerConnMap{}; auto threadPool = DispatchQueue("Main", 1); -const std::string signaling_serverip = "127.0.0.1"; -const int signaling_serverport = 8000; +const std::string signaling_serverip = "10.196.28.10"; +const int signaling_serverport = 8888; static inline bool isDisconnectedState(const rtc::PeerConnection::State& state) { return state == rtc::PeerConnection::State::Disconnected || @@ -51,12 +51,32 @@ std::shared_ptr createPeerConnection( if (isDisconnectedState(state)) { // remove disconnected client threadPool.dispatch([id]() { - clients.erase(id); + peerConnMap.erase(id); }); } } ); + // pc->onLocalDescription( + // [wws, id](rtc::Description description) { + // json message = {{"id", id}, + // {"type", description.typeString()}, + // {"description", std::string(description)}}; + + // if (auto ws = wws.lock()) + // ws->send(message.dump()); + // }); + + pc->onLocalCandidate( + [wws, id](rtc::Candidate candidate) { + json message = {{"id", id}, + {"type", "candidate"}, + {"candidate", std::string(candidate)}}; + + if (auto ws = wws.lock()) + ws->send(message.dump()); + }); + pc->onGatheringStateChange( [wpc = make_weak_ptr(pc), id, wws](rtc::PeerConnection::GatheringState state) { std::cout << "Gathering State: " << state << std::endl; @@ -100,8 +120,10 @@ void handleOffer( const rtc::Configuration& config, const std::shared_ptr ws) { + std::cout << "Got offer request answering to " + id << std::endl; + // create peerconnection - clients.emplace(id, createPeerConnection(id, config, make_weak_ptr(ws))); + peerConnMap.emplace(id, createPeerConnection(id, config, make_weak_ptr(ws))); } void handleWSMsg( @@ -128,26 +150,33 @@ void handleWSMsg( auto type = it->get(); - if (type == "offer") { - // TODO handle offer - handleOffer(id, config, ws); - - // create peer connection + auto peer = std::shared_ptr(); - // gen local desc - - // gen answer + if (auto jt = peerConnMap.find(id); jt != peerConnMap.end()) { + peer = jt->second; + } else if (type == "offer") { + handleOffer(id, config, ws); + peer = peerConnMap[id]; + } else { + return; + } - // send answer - } else if (type == "answer") { - // TODO + if (type == "offer" || type == "answer") { + auto sdp = message["sdp"].get(); + peer->peerConnection->setRemoteDescription(rtc::Description(sdp, type)); + } else if (type == "candidate") { + /* FIXME: avoid nested objects! */ + auto candidates = message["candidate"]["candidate"].get(); + peer->peerConnection->addRemoteCandidate(rtc::Candidate(candidates, "0")); // 0 for now } else if (type == "leave") { // TODO + std::cout << "connection failed due to: " << type << std::endl; } else if (type == "userbusy") { // TODO + std::cout << "connection failed due to: " << type << std::endl; } else if (type == "useroffline") { // TODO - std::cout << "connection failed due to peer is offline: " << type << std::endl; + std::cout << "connection failed due to: " << type << std::endl; } else { std::cout << "unknown message type: " << type << std::endl; } @@ -164,8 +193,8 @@ int main(int argc, char **argv) try { auto config = rtc::Configuration(); config.disableAutoNegotiation = true; // not setting stun server for now - // auto stunServer = std::string("stun:stun.l.google.com:19302"); - // config.iceServers.emplace_back(stunServer); + auto stunServer = std::string("stun:stun.l.google.com:19302"); + config.iceServers.emplace_back(stunServer); // parse the client id from the cmd line auto localid = std::string(argv[1]); From 02bc15e29b8ebadd023c75b6781b8659e22ed5be Mon Sep 17 00:00:00 2001 From: gaojiawei Date: Thu, 16 Nov 2023 22:20:29 +0800 Subject: [PATCH 11/12] fix the incorrect signalling procesure Signed-off-by: gaojiawei --- examples/videocall/main.cpp | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/examples/videocall/main.cpp b/examples/videocall/main.cpp index 498366692..7bac023f8 100644 --- a/examples/videocall/main.cpp +++ b/examples/videocall/main.cpp @@ -57,15 +57,15 @@ std::shared_ptr createPeerConnection( } ); - // pc->onLocalDescription( - // [wws, id](rtc::Description description) { - // json message = {{"id", id}, - // {"type", description.typeString()}, - // {"description", std::string(description)}}; + pc->onLocalDescription( + [wws, id](rtc::Description description) { + json message = {{"id", id}, + {"type", description.typeString()}, + {"description", std::string(description)}}; - // if (auto ws = wws.lock()) - // ws->send(message.dump()); - // }); + if (auto ws = wws.lock()) + ws->send(message.dump()); + }); pc->onLocalCandidate( [wws, id](rtc::Candidate candidate) { @@ -110,7 +110,7 @@ std::shared_ptr createPeerConnection( auto track = pc->addTrack(media); - pc->setLocalDescription(); + // pc->setLocalDescription(); return client; } @@ -164,6 +164,8 @@ void handleWSMsg( if (type == "offer" || type == "answer") { auto sdp = message["sdp"].get(); peer->peerConnection->setRemoteDescription(rtc::Description(sdp, type)); + /* now create the answer */ + peer->peerConnection->setLocalDescription(); } else if (type == "candidate") { /* FIXME: avoid nested objects! */ auto candidates = message["candidate"]["candidate"].get(); From 350cc9afc2bd18acfa1ceb31c80de1d9e417c191 Mon Sep 17 00:00:00 2001 From: gaojiawei Date: Fri, 17 Nov 2023 11:50:35 +0800 Subject: [PATCH 12/12] use portable macro to access private members --- src/impl/dtlssrtptransport.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/impl/dtlssrtptransport.cpp b/src/impl/dtlssrtptransport.cpp index 3a1e30950..8566338ab 100644 --- a/src/impl/dtlssrtptransport.cpp +++ b/src/impl/dtlssrtptransport.cpp @@ -269,7 +269,7 @@ void DtlsSrtpTransport::postHandshake() { mbedtls_dtls_srtp_info srtpInfo; mbedtls_ssl_get_dtls_srtp_negotiation_result(&mSsl, &srtpInfo); - if (srtpInfo.private_chosen_dtls_srtp_profile != MBEDTLS_TLS_SRTP_AES128_CM_HMAC_SHA1_80) + if (srtpInfo.MBEDTLS_PRIVATE(chosen_dtls_srtp_profile) != MBEDTLS_TLS_SRTP_AES128_CM_HMAC_SHA1_80) throw std::runtime_error("Failed to get SRTP profile"); const srtp_profile_t srtpProfile = srtp_profile_aes128_cm_sha1_80;