Commit 2fba5d08 by Christoph

Experimental! Added first video_input prototype and introduced the Media class…

Experimental! Added first video_input prototype and introduced the Media class to abstract all further access to Media devices * new VideoInput class allows adding canvas elements that are then treated like a video device similar to the DeviceAPI * a new canvas can be added as video device via Media.SharedInstance.VideoInput.AddCanvasDevice("canvas", canvas); * the new Media class delegates getUserMedia and GetVideoDevices either to DeviceApi or VideoInput depending on the device names used * the Unity plugin uses the Media class now instead of the DeviceApi * On startup the unity plugin will get the first canvas element in the document and add it to VideoInput for testing via Unity (there is no C# API yet) * added tests for the new features + general cleanup to allow the tests to run in firefox with fake media turned on
parent 7f6f5ccc
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta charset="utf-8" />
<title></title>
<script src="./bundle/apps.js"></script>
</head>
<body>
<div id="videoinputapp1">
<h1>Callapp1:</h1>
URL to connect:
<p class="callapp_url">
</p>
<input type="checkbox" name="audio" class="callapp_send_audio" checked autocomplete="off"> Audio
<input type="checkbox" name="video" class="callapp_send_video" checked autocomplete="off"> Video
<input type= "text" class="callapp_address" autocomplete="off">
<button class="callapp_button"> Join </button>
<div class="callapp_local_video">local video</div>
<div class="callapp_remote_video">remote video</div>
</div>
<canvas id="canvas1"> </canvas>
<script>
var rgbToHex = function (rgb) {
var hex = Number(rgb).toString(16);
if (hex.length < 2) {
hex = "0" + hex;
}
return hex;
};
const canvas = document.querySelector("#canvas1");
const ctx = canvas.getContext("2d");
let counter = 0;
setInterval(()=>{
const color = "#FFFF" + rgbToHex(counter%255);
ctx.fillStyle = color;
ctx.fillRect(0, 0, canvas.width, canvas.height);
counter++;
}, 50);
apps.videoinputapp(document.querySelector("#videoinputapp1"), canvas);
//apps.callapp(document.querySelector("#callapp2"));
</script>
</body>
</html>
\ No newline at end of file
...@@ -11,6 +11,7 @@ ...@@ -11,6 +11,7 @@
"tsc": "tsc", "tsc": "tsc",
"webpack": "webpack", "webpack": "webpack",
"build": "webpack && tsc -p ./src/awrtc", "build": "webpack && tsc -p ./src/awrtc",
"watch": "webpack --watch",
"clean": "shx rm -rf ./build/awrtc ./build/bundle" "clean": "shx rm -rf ./build/awrtc ./build/bundle"
}, },
"devDependencies": { "devDependencies": {
......
...@@ -29,7 +29,6 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ...@@ -29,7 +29,6 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/ */
import * as awrtc from "../awrtc/index" import * as awrtc from "../awrtc/index"
import { ConnectionId } from "../awrtc/index";
/** /**
* Main (and most complicated) example for using BrowserWebRtcCall. * Main (and most complicated) example for using BrowserWebRtcCall.
...@@ -357,7 +356,7 @@ export class CallApp ...@@ -357,7 +356,7 @@ export class CallApp
this.mUiLocalVideoParent.appendChild(video); this.mUiLocalVideoParent.appendChild(video);
} }
private Ui_OnRemoteVideo(video : HTMLVideoElement, id: ConnectionId){ private Ui_OnRemoteVideo(video : HTMLVideoElement, id: awrtc.ConnectionId){
this.mUiRemoteVideoParent.appendChild( document.createElement("br")); this.mUiRemoteVideoParent.appendChild( document.createElement("br"));
this.mUiRemoteVideoParent.appendChild(new Text("connection " + id.id)); this.mUiRemoteVideoParent.appendChild(new Text("connection " + id.id));
......
...@@ -30,4 +30,5 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ...@@ -30,4 +30,5 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
export * from "./apphelpers" export * from "./apphelpers"
export * from "./testapps" export * from "./testapps"
export * from "./examples" export * from "./examples"
export * from "./callapp" export * from "./callapp"
\ No newline at end of file export * from "./videoinputapp"
\ No newline at end of file
import * as awrtc from "../awrtc/index"
/**
* Copy of the CallApp to test custom video input
*/
export class VideoInputApp
{
public static sVideoDevice = null;
private mAddress;
private mNetConfig = new awrtc.NetworkConfig();
private mCall : awrtc.BrowserWebRtcCall = null;
//update loop
private mIntervalId:any = -1;
private mLocalVideo: HTMLVideoElement = null;
private mRemoteVideo = {};
private mIsRunning = false;
public constructor()
{
this.mNetConfig.IceServers = [
{urls: "stun:stun.because-why-not.com:443"},
{urls: "stun:stun.l.google.com:19302"}
];
//use for testing conferences
//this.mNetConfig.IsConference = true;
//this.mNetConfig.SignalingUrl = "wss://signaling.because-why-not.com/testshared";
this.mNetConfig.IsConference = false;
this.mNetConfig.SignalingUrl = "wss://signaling.because-why-not.com/callapp";
}
private GetParameterByName(name) {
var url = window.location.href;
name = name.replace(/[\[\]]/g, "\\$&");
var regex = new RegExp("[?&]" + name + "(=([^&#]*)|&|#|$)"), results = regex.exec(url);
if (!results)
return null;
if (!results[2])
return '';
return decodeURIComponent(results[2].replace(/\+/g, " "));
}
private tobool(value, defaultval)
{
if(value === true || value === "true")
return true;
if(value === false || value === "false")
return false;
return defaultval;
}
public Start(address, audio, video) : void
{
if(this.mCall != null)
this.Stop();
this.mIsRunning = true;
this.Ui_OnStart()
console.log("start");
console.log("Using signaling server url: " + this.mNetConfig.SignalingUrl);
//create media configuration
var config = new awrtc.MediaConfig();
config.Audio = audio;
config.Video = video;
config.IdealWidth = 640;
config.IdealHeight = 480;
config.IdealFps = 30;
if(VideoInputApp.sVideoDevice !== null)
{
config.VideoDeviceName = VideoInputApp.sVideoDevice;
}
//For usage in HTML set FrameUpdates to false and wait for MediaUpdate to
//get the VideoElement. By default awrtc would deliver frames individually
//for use in Unity WebGL
console.log("requested config:" + JSON.stringify(config));
//setup our high level call class.
this.mCall = new awrtc.BrowserWebRtcCall(this.mNetConfig);
//handle events (get triggered after Configure / Listen call)
//+ugly lambda to avoid loosing "this" reference
this.mCall.addEventListener((sender, args)=>{
this.OnNetworkEvent(sender, args);
});
//As the system is designed for realtime graphics we have to call the Update method. Events are only
//triggered during this Update call!
this.mIntervalId = setInterval(()=>{
this.Update();
}, 50);
//configure media. This will request access to media and can fail if the user doesn't have a proper device or
//blocks access
this.mCall.Configure(config);
//Try to listen to the address
//Conference mode = everyone listening will connect to each other
//Call mode -> If the address is free it will wait for someone else to connect
// -> If the address is used then it will fail to listen and then try to connect via Call(address);
this.mCall.Listen(address);
}
public Stop(): void
{
this.Cleanup();
}
private Cleanup():void{
if(this.mCall != null)
{
this.mCall.Dispose();
this.mCall = null;
clearInterval(this.mIntervalId);
this.mIntervalId = -1;
this.mIsRunning = false;
this.mLocalVideo = null;
this.mRemoteVideo = {};
}
this.Ui_OnCleanup();
}
private Update():void
{
if(this.mCall != null)
this.mCall.Update();
}
private OnNetworkEvent(sender: any, args: awrtc.CallEventArgs):void{
//User gave access to requested camera/ microphone
if (args.Type == awrtc.CallEventType.ConfigurationComplete){
console.log("configuration complete");
}
else if (args.Type == awrtc.CallEventType.MediaUpdate) {
let margs = args as awrtc.MediaUpdatedEventArgs;
if (this.mLocalVideo == null && margs.ConnectionId == awrtc.ConnectionId.INVALID) {
var videoElement = margs.VideoElement;
this.mLocalVideo = videoElement;
this.Ui_OnLocalVideo(videoElement);
console.log("local video added resolution:" + videoElement.videoWidth + videoElement.videoHeight + " fps: ??");
}
else if (margs.ConnectionId != awrtc.ConnectionId.INVALID && this.mRemoteVideo[margs.ConnectionId.id] == null) {
var videoElement = margs.VideoElement;
this.mRemoteVideo[margs.ConnectionId.id] = videoElement;
this.Ui_OnRemoteVideo(videoElement, margs.ConnectionId);
console.log("remote video added resolution:" + videoElement.videoWidth + videoElement.videoHeight + " fps: ??");
}
}
else if (args.Type == awrtc.CallEventType.ListeningFailed) {
//First attempt of this example is to try to listen on a certain address
//for conference calls this should always work (expect the internet is dead)
if (this.mNetConfig.IsConference == false) {
//no conference call and listening failed? someone might have claimed the address.
//Try to connect to existing call
this.mCall.Call(this.mAddress);
}
else {
let errorMsg = "Listening failed. Offline? Server dead?";
console.error(errorMsg);
this.Ui_OnError(errorMsg);
this.Cleanup();
return;
}
}
else if (args.Type == awrtc.CallEventType.ConnectionFailed) {
//Outgoing call failed entirely. This can mean there is no address to connect to,
//server is offline, internet is dead, firewall blocked access, ...
let errorMsg = "Connection failed. Offline? Server dead? ";
console.error(errorMsg);
this.Ui_OnError(errorMsg);
this.Cleanup();
return;
}
else if (args.Type == awrtc.CallEventType.CallEnded) {
//call ended or was disconnected
var callEndedEvent = args as awrtc.CallEndedEventArgs;
console.log("call ended with id " + callEndedEvent.ConnectionId.id);
delete this.mRemoteVideo[callEndedEvent.ConnectionId.id];
this.Ui_OnLog("Disconnected from user with id " + callEndedEvent.ConnectionId.id);
//check if this was the last user
if(this.mNetConfig.IsConference == false && Object.keys(this.mRemoteVideo).length == 0)
{
//1 to 1 call and only user left -> quit
this.Cleanup();
return;
}
}
else if (args.Type == awrtc.CallEventType.Message) {
//no ui for this yet. simply echo messages for testing
let messageArgs = args as awrtc.MessageEventArgs;
this.mCall.Send(messageArgs.Content, messageArgs.Reliable, messageArgs.ConnectionId);
}
else if (args.Type == awrtc.CallEventType.DataMessage) {
//no ui for this yet. simply echo messages for testing
let messageArgs = args as awrtc.DataMessageEventArgs;
this.mCall.SendData(messageArgs.Content, messageArgs.Reliable, messageArgs.ConnectionId);
}
else if (args.Type == awrtc.CallEventType.CallAccepted) {
let arg = args as awrtc.CallAcceptedEventArgs;
console.log("New call accepted id: " + arg.ConnectionId.id);
}
else if (args.Type == awrtc.CallEventType.WaitForIncomingCall) {
console.log("Waiting for incoming call ...");
}
else {
console.log("Unhandled event: " + args.Type);
}
}
//UI calls. should be moved out into its own class later
private mAudio;
private mVideo;
private mAutostart;
private mUiAddress: HTMLInputElement;
private mUiAudio: HTMLInputElement;
private mUiVideo: HTMLInputElement;
private mUiButton: HTMLButtonElement;
private mUiUrl: HTMLElement;
private mUiLocalVideoParent: HTMLElement;
private mUiRemoteVideoParent: HTMLElement;
public setupUi(parent : HTMLElement)
{
this.mUiAddress = parent.querySelector<HTMLInputElement>(".callapp_address");
this.mUiAudio = parent.querySelector<HTMLInputElement>(".callapp_send_audio");
this.mUiVideo = parent.querySelector<HTMLInputElement>(".callapp_send_video");
this.mUiUrl = parent.querySelector<HTMLParagraphElement>(".callapp_url");
this.mUiButton = parent.querySelector<HTMLInputElement>(".callapp_button");
this.mUiLocalVideoParent = parent.querySelector<HTMLParagraphElement>(".callapp_local_video");
this.mUiRemoteVideoParent = parent.querySelector<HTMLParagraphElement>(".callapp_remote_video");
this.mUiAudio.onclick = this.Ui_OnUpdate;
this.mUiVideo.onclick = this.Ui_OnUpdate;
this.mUiAddress.onkeyup = this.Ui_OnUpdate;
this.mUiButton.onclick = this.Ui_OnStartStopButtonClicked;
//set default value + make string "true"/"false" to proper booleans
this.mAudio = this.GetParameterByName("audio");
this.mAudio = this.tobool(this.mAudio , true)
this.mVideo = this.GetParameterByName("video");
this.mVideo = this.tobool(this.mVideo , true);
this.mAutostart = this.GetParameterByName("autostart");
this.mAutostart = this.tobool(this.mAutostart, false);
this.mAddress = this.GetParameterByName("a");
//if autostart is set but no address is given -> create one and reopen the page
if (this.mAddress === null && this.mAutostart == true) {
this.mAddress = this.GenerateRandomKey();
window.location.href = this.GetUrlParams();
}
else
{
if(this.mAddress === null)
this.mAddress = this.GenerateRandomKey();
this.Ui_Update();
}
//used for interacting with the Unity CallApp
//current hack to get the html element delivered. by default this
//just the image is copied and given as array
//Lazy frames will be the default soon though
if(this.mAutostart)
{
console.log("Starting automatically ... ")
this.Start(this.mAddress, this.mAudio , this.mVideo );
}
console.log("address: " + this.mAddress + " audio: " + this.mAudio + " video: " + this.mVideo + " autostart: " + this.mAutostart);
}
private Ui_OnStart(){
this.mUiButton.textContent = "Stop";
}
private Ui_OnCleanup()
{
this.mUiButton.textContent = "Join";
while (this.mUiLocalVideoParent.hasChildNodes()) {
this.mUiLocalVideoParent.removeChild(this.mUiLocalVideoParent.firstChild);
}
while (this.mUiRemoteVideoParent.hasChildNodes()) {
this.mUiRemoteVideoParent.removeChild(this.mUiRemoteVideoParent.firstChild);
}
}
private Ui_OnLog(msg:string){
}
private Ui_OnError(msg:string){
}
private Ui_OnLocalVideo(video : HTMLVideoElement){
this.mUiLocalVideoParent.appendChild( document.createElement("br"));
this.mUiLocalVideoParent.appendChild(video);
}
private Ui_OnRemoteVideo(video : HTMLVideoElement, id: awrtc.ConnectionId){
this.mUiRemoteVideoParent.appendChild( document.createElement("br"));
this.mUiRemoteVideoParent.appendChild(new Text("connection " + id.id));
this.mUiRemoteVideoParent.appendChild( document.createElement("br"));
this.mUiRemoteVideoParent.appendChild(video);
}
public Ui_OnStartStopButtonClicked = ()=>{
if(this.mIsRunning) {
this.Stop();
}else{
this.Start(this.mAddress, this.mAudio, this.mVideo);
}
}
public Ui_OnUpdate = ()=>
{
console.debug("OnUiUpdate");
this.mAddress = this.mUiAddress.value;
this.mAudio = this.mUiAudio.checked;
this.mVideo = this.mUiVideo.checked;
this.mUiUrl.innerHTML = this.GetUrl();
}
public Ui_Update() : void
{
console.log("UpdateUi");
this.mUiAddress.value = this.mAddress;
this.mUiAudio.checked = this.mAudio ;
this.mUiVideo.checked = this.mVideo ;
this.mUiUrl.innerHTML = this.GetUrl();
}
private GenerateRandomKey() {
var result = "";
for (var i = 0; i < 7; i++) {
result += String.fromCharCode(65 + Math.round(Math.random() * 25));
}
return result;
}
private GetUrlParams() {
return "?a=" + this.mAddress + "&audio=" + this.mAudio + "&video=" + this.mVideo + "&" + "autostart=" + true;
}
private GetUrl() {
return location.protocol + '//' + location.host + location.pathname + this.GetUrlParams();
}
}
export function videoinputapp(parent: HTMLElement, canvas: HTMLCanvasElement)
{
let callApp : VideoInputApp;
console.log("init callapp");
if(parent == null)
{
console.log("parent was null");
parent = document.body;
}
awrtc.SLog.SetLogLevel(awrtc.SLogLevel.Info);
callApp = new VideoInputApp();
const media = new awrtc.Media();
const devname = "canvas";
awrtc.Media.SharedInstance.VideoInput.AddCanvasDevice(devname, canvas);
VideoInputApp.sVideoDevice = devname;
callApp.setupUi(parent);
}
/*
Copyright (c) 2019, because-why-not.com Limited
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
* Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
//obsolete. not needed for unity build anymore
//special entry point only needed for backwards compatibility
//it will merge awrtc namespace into window so old code still works
//that accesses objects directly instead using the global awrtc object
//the index will include all external modules
import * as awrtc from "./index"
//we merge awrtc into the global namespace
Object.assign(window, awrtc);
//for less risky global access
(window as any).awrtc = awrtc;
...@@ -42,4 +42,5 @@ export * from "./media/index" ...@@ -42,4 +42,5 @@ export * from "./media/index"
//it could as well be built and deployed separately //it could as well be built and deployed separately
export * from "./media_browser/index" export * from "./media_browser/index"
export * from "./unity/index" export * from "./unity/index"
console.debug("loading awrtc modules completed"); console.debug("loading awrtc modules completed!");
\ No newline at end of file
...@@ -36,6 +36,7 @@ import { IFrameData } from "../media/RawFrame"; ...@@ -36,6 +36,7 @@ import { IFrameData } from "../media/RawFrame";
import { MediaPeer } from "./MediaPeer"; import { MediaPeer } from "./MediaPeer";
import { BrowserMediaStream } from "./BrowserMediaStream"; import { BrowserMediaStream } from "./BrowserMediaStream";
import { DeviceApi } from "./DeviceApi"; import { DeviceApi } from "./DeviceApi";
import { Media } from "./Media";
/**Avoid using this class directly whenever possible. Use BrowserWebRtcCall instead. /**Avoid using this class directly whenever possible. Use BrowserWebRtcCall instead.
...@@ -60,6 +61,7 @@ import { DeviceApi } from "./DeviceApi"; ...@@ -60,6 +61,7 @@ import { DeviceApi } from "./DeviceApi";
*/ */
export class BrowserMediaNetwork extends WebRtcNetwork implements IMediaNetwork { export class BrowserMediaNetwork extends WebRtcNetwork implements IMediaNetwork {
//media configuration set by the user //media configuration set by the user
private mMediaConfig: MediaConfig = null; private mMediaConfig: MediaConfig = null;
//keeps track of audio / video tracks based on local devices //keeps track of audio / video tracks based on local devices
...@@ -69,6 +71,7 @@ export class BrowserMediaNetwork extends WebRtcNetwork implements IMediaNetwork ...@@ -69,6 +71,7 @@ export class BrowserMediaNetwork extends WebRtcNetwork implements IMediaNetwork
private mConfigurationError: string = null; private mConfigurationError: string = null;
private mMediaEvents: Queue<MediaEvent> = new Queue<MediaEvent>(); private mMediaEvents: Queue<MediaEvent> = new Queue<MediaEvent>();
constructor(config: NetworkConfig) { constructor(config: NetworkConfig) {
super(BrowserMediaNetwork.BuildSignalingConfig(config.SignalingUrl), super(BrowserMediaNetwork.BuildSignalingConfig(config.SignalingUrl),
...@@ -91,102 +94,13 @@ export class BrowserMediaNetwork extends WebRtcNetwork implements IMediaNetwork ...@@ -91,102 +94,13 @@ export class BrowserMediaNetwork extends WebRtcNetwork implements IMediaNetwork
if (config.Audio || config.Video) { if (config.Audio || config.Video) {
SLog.L("calling GetUserMedia. Media config: " + JSON.stringify(config));
//ugly part starts -> call get user media data (no typescript support) if(DeviceApi.IsUserMediaAvailable())
//different browsers have different calls...
//check getSupportedConstraints()???
//see https://w3c.github.io/mediacapture-main/getusermedia.html#constrainable-interface
//set default ideal to very common low 320x240 to avoid overloading weak computers
var constraints = {
audio: config.Audio
} as any;
let width = {} as any;
let height = {} as any;
let video = {} as any;
let fps = {} as any;
if (config.MinWidth != -1)
width.min = config.MinWidth;
if (config.MaxWidth != -1)
width.max = config.MaxWidth;
if (config.IdealWidth != -1)
width.ideal = config.IdealWidth;
if (config.MinHeight != -1)
height.min = config.MinHeight;
if (config.MaxHeight != -1)
height.max = config.MaxHeight;
if (config.IdealHeight != -1)
height.ideal = config.IdealHeight;
if (config.MinFps != -1)
fps.min = config.MinFps;
if (config.MaxFps != -1)
fps.max = config.MaxFps;
if (config.IdealFps != -1)
fps.ideal = config.IdealFps;
//user requested specific device? get it now to properly add it to the
//constraints later
let deviceId:string = null;
if(config.Video && config.VideoDeviceName && config.VideoDeviceName !== "")
{
deviceId = DeviceApi.GetDeviceId(config.VideoDeviceName);
SLog.L("using device " + config.VideoDeviceName);
if(deviceId !== null)
{
//SLog.L("using device id " + deviceId);
}
else{
SLog.LE("Failed to find deviceId for label " + config.VideoDeviceName);
}
}
//watch out: unity changed behaviour and will now
//give 0 / 1 instead of false/true
//using === won't work
if(config.Video == false)
{ {
//video is off let promise : Promise<MediaStream> = null;
video = false; promise = Media.SharedInstance.getUserMedia(config);
}else {
if(Object.keys(width).length > 0){
video.width = width;
}
if(Object.keys(height).length > 0){
video.height = height;
}
if(Object.keys(fps).length > 0){
video.frameRate = fps;
}
if(deviceId !== null){
video.deviceId = {"exact":deviceId};
}
//if we didn't add anything we need to set it to true
//at least (I assume?)
if(Object.keys(video).length == 0){
video = true;
}
}
constraints.video = video;
SLog.L("calling GetUserMedia. Media constraints: " + JSON.stringify(constraints));
if(navigator && navigator.mediaDevices)
{
let promise = navigator.mediaDevices.getUserMedia(constraints);
promise.then((stream) => { //user gave permission promise.then((stream) => { //user gave permission
//totally unrelated -> user gave access to devices. use this //totally unrelated -> user gave access to devices. use this
...@@ -195,6 +109,7 @@ export class BrowserMediaNetwork extends WebRtcNetwork implements IMediaNetwork ...@@ -195,6 +109,7 @@ export class BrowserMediaNetwork extends WebRtcNetwork implements IMediaNetwork
//call worked -> setup a frame buffer that deals with the rest //call worked -> setup a frame buffer that deals with the rest
this.mLocalStream = new BrowserMediaStream(stream as MediaStream); this.mLocalStream = new BrowserMediaStream(stream as MediaStream);
//console.debug("Local tracks: ", stream.getTracks());
this.mLocalStream.InternalStreamAdded = (stream)=>{ this.mLocalStream.InternalStreamAdded = (stream)=>{
this.EnqueueMediaEvent(MediaEventType.StreamAdded, ConnectionId.INVALID, this.mLocalStream.VideoElement); this.EnqueueMediaEvent(MediaEventType.StreamAdded, ConnectionId.INVALID, this.mLocalStream.VideoElement);
}; };
......
...@@ -28,6 +28,8 @@ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE ...@@ -28,6 +28,8 @@ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/ */
import { SLog } from "../network/index"; import { SLog } from "../network/index";
import { MediaConfig } from "media/MediaConfig";
import { VideoInput } from "./VideoInput";
export class DeviceInfo export class DeviceInfo
{ {
...@@ -79,7 +81,11 @@ export class DeviceApi ...@@ -79,7 +81,11 @@ export class DeviceApi
{ {
let index = DeviceApi.sUpdateEvents.indexOf(evt); let index = DeviceApi.sUpdateEvents.indexOf(evt);
if(index >= 0) if(index >= 0)
{
DeviceApi.sUpdateEvents.splice(index, 1); DeviceApi.sUpdateEvents.splice(index, 1);
}else{
SLog.LW("Tried to remove an unknown event handler in DeviceApi.RemOnChangedHandler");
}
} }
private static TriggerChangedEvent() private static TriggerChangedEvent()
...@@ -166,6 +172,15 @@ export class DeviceApi ...@@ -166,6 +172,15 @@ export class DeviceApi
{ {
return DeviceApi.sDeviceInfo; return DeviceApi.sDeviceInfo;
} }
public static GetVideoDevices(): string[]{
const devices = DeviceApi.Devices;
const keys = Object.keys(devices);
const labels = keys.map((x)=>{return devices[x].label});
return labels;
}
public static Reset() public static Reset()
{ {
DeviceApi.sUpdateEvents = []; DeviceApi.sUpdateEvents = [];
...@@ -196,7 +211,7 @@ export class DeviceApi ...@@ -196,7 +211,7 @@ export class DeviceApi
DeviceApi.Update(); DeviceApi.Update();
} }
static ENUM_FAILED = "Can't access mediaDevices or enumerateDevices";
/**Updates the device list based on the current /**Updates the device list based on the current
* access. Gives the devices numbers if the name isn't known. * access. Gives the devices numbers if the name isn't known.
*/ */
...@@ -210,9 +225,28 @@ export class DeviceApi ...@@ -210,9 +225,28 @@ export class DeviceApi
.then(DeviceApi.InternalOnEnum) .then(DeviceApi.InternalOnEnum)
.catch(DeviceApi.InternalOnErrorCatch); .catch(DeviceApi.InternalOnErrorCatch);
}else{ }else{
DeviceApi.InternalOnErrorString("Can't access mediaDevices or enumerateDevices"); DeviceApi.InternalOnErrorString(DeviceApi.ENUM_FAILED);
} }
} }
public static async UpdateAsync():Promise<void>
{
return new Promise((resolve, fail)=>{
DeviceApi.sLastError = null;
if(DeviceApi.IsApiAvailable() == false)
{
DeviceApi.InternalOnErrorString(DeviceApi.ENUM_FAILED);
fail(DeviceApi.ENUM_FAILED);
}
resolve();
}).then(()=>{
DeviceApi.sIsPending = true;
return navigator.mediaDevices.enumerateDevices()
.then(DeviceApi.InternalOnEnum)
.catch(DeviceApi.InternalOnErrorCatch);
});
}
/**Checks if the API is available in the browser. /**Checks if the API is available in the browser.
* false - browser doesn't support this API * false - browser doesn't support this API
* true - browser supports the API (might still refuse to give * true - browser supports the API (might still refuse to give
...@@ -255,4 +289,122 @@ export class DeviceApi ...@@ -255,4 +289,122 @@ export class DeviceApi
} }
return null; return null;
} }
public static IsUserMediaAvailable()
{
if(navigator && navigator.mediaDevices)
return true;
return false;
}
public static ToConstraints(config: MediaConfig): MediaStreamConstraints
{
//ugly part starts -> call get user media data (no typescript support)
//different browsers have different calls...
//check getSupportedConstraints()???
//see https://w3c.github.io/mediacapture-main/getusermedia.html#constrainable-interface
//set default ideal to very common low 320x240 to avoid overloading weak computers
var constraints = {
audio: config.Audio
} as any;
let width = {} as any;
let height = {} as any;
let video = {} as any;
let fps = {} as any;
if (config.MinWidth != -1)
width.min = config.MinWidth;
if (config.MaxWidth != -1)
width.max = config.MaxWidth;
if (config.IdealWidth != -1)
width.ideal = config.IdealWidth;
if (config.MinHeight != -1)
height.min = config.MinHeight;
if (config.MaxHeight != -1)
height.max = config.MaxHeight;
if (config.IdealHeight != -1)
height.ideal = config.IdealHeight;
if (config.MinFps != -1)
fps.min = config.MinFps;
if (config.MaxFps != -1)
fps.max = config.MaxFps;
if (config.IdealFps != -1)
fps.ideal = config.IdealFps;
//user requested specific device? get it now to properly add it to the
//constraints later
let deviceId:string = null;
if(config.Video && config.VideoDeviceName && config.VideoDeviceName !== "")
{
deviceId = DeviceApi.GetDeviceId(config.VideoDeviceName);
SLog.L("using device " + config.VideoDeviceName);
if(deviceId !== null)
{
//SLog.L("using device id " + deviceId);
}
else{
SLog.LE("Failed to find deviceId for label " + config.VideoDeviceName);
throw new Error("Unknown device " + config.VideoDeviceName);
}
}
//watch out: unity changed behaviour and will now
//give 0 / 1 instead of false/true
//using === won't work
if(config.Video == false)
{
//video is off
video = false;
}else {
if(Object.keys(width).length > 0){
video.width = width;
}
if(Object.keys(height).length > 0){
video.height = height;
}
if(Object.keys(fps).length > 0){
video.frameRate = fps;
}
if(deviceId !== null){
video.deviceId = {"exact":deviceId};
}
//if we didn't add anything we need to set it to true
//at least (I assume?)
if(Object.keys(video).length == 0){
video = true;
}
}
constraints.video = video;
return constraints;
}
public static getBrowserUserMedia(constraints?: MediaStreamConstraints): Promise<MediaStream>{
return navigator.mediaDevices.getUserMedia(constraints);
}
public static getAssetUserMedia(config: MediaConfig): Promise<MediaStream>{
return new Promise((resolve)=>{
const res = DeviceApi.ToConstraints(config);
resolve(res);
}).then((constraints)=>{
return DeviceApi.getBrowserUserMedia(constraints as MediaStreamConstraints);
});
}
} }
\ No newline at end of file
import { DeviceApi } from "./DeviceApi";
import { VideoInput } from "./VideoInput";
import { MediaConfig } from "media/MediaConfig";
export class Media{
//experimental. Will be used instead of the device api to create streams
private static sSharedInstance :Media = new Media();
/**
* Singleton used for now as the browser version is missing a proper factory yet.
* Might be removed later.
*/
public static get SharedInstance(){
return this.sSharedInstance;
}
public static ResetSharedInstance(){
this.sSharedInstance = new Media();
}
private videoInput: VideoInput = null;
public get VideoInput() : VideoInput{
if(this.videoInput === null)
this.videoInput = new VideoInput();
return this.videoInput;
}
public constructor(){
}
public GetVideoDevices(): string[] {
const real_devices = DeviceApi.GetVideoDevices();
const virtual_devices : string[] = this.VideoInput.GetDeviceNames();
return real_devices.concat(virtual_devices);
}
public getUserMedia(config: MediaConfig): Promise<MediaStream>{
if(config.VideoDeviceName !== null
&& config.VideoDeviceName !== ""
&& this.videoInput != null
&& this.videoInput.HasDevice(config.VideoDeviceName))
{
return new Promise<MediaStream>((resolve, reject) => {
try{
const res :MediaStream = this.videoInput.GetStream(config.VideoDeviceName);
resolve(res)
}catch(err)
{
reject(err);
}
});
}
return DeviceApi.getAssetUserMedia(config);
}
}
\ No newline at end of file
interface CanvasMap{
[key:string] : HTMLCanvasElement;
}
export class VideoInput{
private canvasDevices : CanvasMap = {};
constructor(){
}
public AddCanvasDevice(deviceName: string, canvas : HTMLCanvasElement){
this.canvasDevices[deviceName] = canvas;
}
public RemCanvasDevice(deviceName: string){
delete this.canvasDevices[deviceName];
}
public HasDevice(dev: string): boolean{
return dev in this.canvasDevices;
}
public GetStream(dev: string) : MediaStream | null
{
if(this.HasDevice(dev)){
let canvas = this.canvasDevices[dev];
//watch out: This can trigger an exception if getContext has never been called before.
//There doesn't seem to way to detect this beforehand though
let stream = (canvas as any).captureStream();
return stream;
}
return null;
}
public GetDeviceNames() : Array<string>{
return Object.keys(this.canvasDevices);
}
}
\ No newline at end of file
...@@ -32,3 +32,5 @@ export * from './BrowserWebRtcCall' ...@@ -32,3 +32,5 @@ export * from './BrowserWebRtcCall'
export * from './BrowserMediaStream' export * from './BrowserMediaStream'
export * from './MediaPeer' export * from './MediaPeer'
export * from './DeviceApi' export * from './DeviceApi'
export * from './VideoInput'
export * from './Media'
...@@ -213,22 +213,42 @@ export class SLog { ...@@ -213,22 +213,42 @@ export class SLog {
SLog.LogError(msg, tag); SLog.LogError(msg, tag);
} }
public static Log(msg: any, tag?:string): void { public static Log(msg: any, tag?:string): void {
if(!tag)
tag = "";
if(SLog.sLogLevel >= SLogLevel.Info) if(SLog.sLogLevel >= SLogLevel.Info)
console.log(msg, tag); {
if(tag)
{
console.log(msg, tag);
}else{
console.log(msg);
}
}
} }
public static LogWarning(msg: any, tag?:string): void { public static LogWarning(msg: any, tag?:string): void {
if(!tag) if(!tag)
tag = ""; tag = "";
if(SLog.sLogLevel >= SLogLevel.Warnings) if(SLog.sLogLevel >= SLogLevel.Warnings)
console.warn(msg, tag); {
if(tag)
{
console.warn(msg, tag);
}else{
console.warn(msg);
}
}
} }
public static LogError(msg: any, tag?:string) { public static LogError(msg: any, tag?:string) {
if(!tag)
tag = "";
if(SLog.sLogLevel >= SLogLevel.Errors) if(SLog.sLogLevel >= SLogLevel.Errors)
console.error(msg, tag); {
if(tag)
{
console.error(msg, tag);
}else{
console.error(msg);
}
}
} }
} }
\ No newline at end of file
...@@ -31,7 +31,10 @@ ...@@ -31,7 +31,10 @@
"./media_browser/BrowserMediaNetwork.ts", "./media_browser/BrowserMediaNetwork.ts",
"./media_browser/BrowserWebRtcCall.ts", "./media_browser/BrowserWebRtcCall.ts",
"./media_browser/BrowserMediaStream.ts", "./media_browser/BrowserMediaStream.ts",
"./media_browser/DeviceApi.ts",
"./media_browser/MediaPeer.ts", "./media_browser/MediaPeer.ts",
"./media_browser/VideoInput.ts",
"./media_browser/Media.ts",
"./media_browser/index.ts", "./media_browser/index.ts",
"./unity/CAPI.ts", "./unity/CAPI.ts",
"./unity/index.ts", "./unity/index.ts",
......
...@@ -32,7 +32,7 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ...@@ -32,7 +32,7 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/ */
import {SLog, WebRtcNetwork, SignalingConfig, NetworkEvent, ConnectionId, LocalNetwork, WebsocketNetwork} from "../network/index" import {SLog, WebRtcNetwork, SignalingConfig, NetworkEvent, ConnectionId, LocalNetwork, WebsocketNetwork} from "../network/index"
import { MediaConfigurationState, NetworkConfig, MediaConfig } from "../media/index"; import { MediaConfigurationState, NetworkConfig, MediaConfig } from "../media/index";
import { BrowserMediaStream, BrowserMediaNetwork, DeviceApi, BrowserWebRtcCall } from "../media_browser/index"; import { BrowserMediaStream, BrowserMediaNetwork, DeviceApi, BrowserWebRtcCall, Media } from "../media_browser/index";
var CAPI_InitMode = { var CAPI_InitMode = {
...@@ -496,7 +496,7 @@ export function CAPI_DeviceApi_LastUpdate():number ...@@ -496,7 +496,7 @@ export function CAPI_DeviceApi_LastUpdate():number
{ {
return DeviceApi.LastUpdate; return DeviceApi.LastUpdate;
} }
/*
export function CAPI_DeviceApi_Devices_Length():number{ export function CAPI_DeviceApi_Devices_Length():number{
return Object.keys(DeviceApi.Devices).length; return Object.keys(DeviceApi.Devices).length;
} }
...@@ -512,4 +512,22 @@ export function CAPI_DeviceApi_Devices_Get(index:number):string{ ...@@ -512,4 +512,22 @@ export function CAPI_DeviceApi_Devices_Get(index:number):string{
SLog.LE("Requested device with index " + index + " does not exist."); SLog.LE("Requested device with index " + index + " does not exist.");
return ""; return "";
} }
}
*/
export function CAPI_DeviceApi_Devices_Length():number{
return Media.SharedInstance.GetVideoDevices().length;
}
export function CAPI_DeviceApi_Devices_Get(index:number):string{
const devs = Media.SharedInstance.GetVideoDevices();
if(devs.length > index)
{
return devs[index];
}
else
{
SLog.LE("Requested device with index " + index + " does not exist.");
//it needs to be "" to behave the same to the C++ API. std::string can't be null
return "";
}
} }
\ No newline at end of file
...@@ -27,4 +27,20 @@ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, ...@@ -27,4 +27,20 @@ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/ */
import { Media } from "../media_browser/Media";
export * from "./CAPI" export * from "./CAPI"
//add the canvas to video input for testing.
//done via timeout to avoid returning possible errors to unity loading routine
setTimeout(()=>{
console.debug("trying to add canvas to video input");
const canvas = document.querySelector("canvas");
if(canvas)
{
Media.SharedInstance.VideoInput.AddCanvasDevice("canvas", canvas);
console.debug("Canvas added. Make sure to turn off unity local video if streaming from a canvas. Copying images from the canvas to Unity will heavily slow down the app!");
}else{
console.error("Adding canvas failed. No canvas found");
}
}, 10);
\ No newline at end of file
...@@ -30,7 +30,9 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ...@@ -30,7 +30,9 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
//current setup needs to load everything as a module //current setup needs to load everything as a module
import {DeviceApi, CAPI_DeviceApi_Update, import {DeviceApi, CAPI_DeviceApi_Update,
CAPI_DeviceApi_RequestUpdate, CAPI_DeviceApi_Devices_Length, CAPI_DeviceApi_RequestUpdate, CAPI_DeviceApi_Devices_Length,
CAPI_DeviceApi_Devices_Get} from "../awrtc/index" CAPI_DeviceApi_Devices_Get,
MediaConfig,
Media} from "../awrtc/index"
export function DeviceApiTest_export() export function DeviceApiTest_export()
{ {
...@@ -132,12 +134,13 @@ describe("DeviceApiTest", () => { ...@@ -132,12 +134,13 @@ describe("DeviceApiTest", () => {
let update2complete = false; let update2complete = false;
let deviceCount = 0; let deviceCount = 0;
expect(CAPI_DeviceApi_Devices_Length()).toBe(0); const devices_length_unitialized = CAPI_DeviceApi_Devices_Length();
CAPI_DeviceApi_Update(); expect(devices_length_unitialized).toBe(0);
DeviceApi.AddOnChangedHandler(()=>{
setTimeout(()=>{ let dev_length = CAPI_DeviceApi_Devices_Length();
expect(CAPI_DeviceApi_Devices_Length()).not.toBe(0); expect(dev_length).not.toBe(0);
expect(CAPI_DeviceApi_Devices_Length()).toBe(Object.keys(DeviceApi.Devices).length); expect(dev_length).toBe(Object.keys(DeviceApi.Devices).length);
let keys = Object.keys(DeviceApi.Devices); let keys = Object.keys(DeviceApi.Devices);
let counter = 0; let counter = 0;
...@@ -150,8 +153,116 @@ describe("DeviceApiTest", () => { ...@@ -150,8 +153,116 @@ describe("DeviceApiTest", () => {
counter++; counter++;
} }
done(); done();
}, 100); });
CAPI_DeviceApi_Update();
}); });
it("isMediaAvailable", () => {
const res = DeviceApi.IsUserMediaAvailable();
expect(res).toBe(true);
});
it("getUserMedia", async () => {
let stream = await DeviceApi.getBrowserUserMedia({audio:true});
expect(stream).not.toBeNull();
expect(stream.getVideoTracks().length).toBe(0);
expect(stream.getAudioTracks().length).toBe(1);
stream = await DeviceApi.getBrowserUserMedia({video:true});
expect(stream).not.toBeNull();
expect(stream.getAudioTracks().length).toBe(0);
expect(stream.getVideoTracks().length).toBe(1);
});
it("getAssetMedia", async () => {
let config = new MediaConfig();
config.Audio = true;
config.Video = false;
let stream = await DeviceApi.getAssetUserMedia(config);
expect(stream).not.toBeNull();
expect(stream.getVideoTracks().length).toBe(0);
expect(stream.getAudioTracks().length).toBe(1);
config = new MediaConfig();
config.Audio = false;
config.Video = true;
stream = await DeviceApi.getAssetUserMedia(config);
expect(stream).not.toBeNull();
expect(stream.getAudioTracks().length).toBe(0);
expect(stream.getVideoTracks().length).toBe(1);
});
it("getAssetMedia_invalid", async () => {
let config = new MediaConfig();
config.Audio = false;
config.Video = true;
config.VideoDeviceName = "invalid name"
let error = null;
let stream :MediaStream = null;
console.log("Expecting error message: Failed to find deviceId for label invalid name");
try
{
stream = await DeviceApi.getAssetUserMedia(config);
}catch(err){
error = err;
}
expect(stream).toBeNull();
expect(error).toBeTruthy();
});
//check for a specific bug causing promise catch not to trigger correctly
//due to error in ToConstraints
it("getAssetMedia_invalid_promise", (done) => {
let config = new MediaConfig();
config.Audio = false;
config.Video = true;
config.VideoDeviceName = "invalid name"
let result: Promise<MediaStream> = null;
result = DeviceApi.getAssetUserMedia(config);
result.then(()=>{
fail("getAssetUserMedia returned but was expected to fail");
}).catch((error)=>{
expect(error).toBeTruthy();
done();
})
});
it("UpdateAsync", async (done) => {
expect(DeviceApi.GetVideoDevices().length).toBe(0);
await DeviceApi.UpdateAsync();
expect(DeviceApi.GetVideoDevices().length).toBeGreaterThan(0);
expect(DeviceApi.GetVideoDevices().length).toBe(CAPI_DeviceApi_Devices_Length());
done();
});
/*
it("Devices", async () => {
DeviceApi.RequestUpdate
let config = new MediaConfig();
config.Audio = false;
config.Video = true;
config.VideoDeviceName = "invalid name"
let error = null;
let stream :MediaStream = null;
console.log("Expecting error message: Failed to find deviceId for label invalid name");
try
{
stream = await DeviceApi.getAssetUserMedia(config);
}catch(err){
error = err;
}
expect(stream).toBeNull();
expect(error).toBeTruthy();
});
*/
}); });
...@@ -29,7 +29,7 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ...@@ -29,7 +29,7 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/ */
import { BrowserMediaNetwork, NetworkConfig, MediaConfig, import { BrowserMediaNetwork, NetworkConfig, MediaConfig,
ConnectionId, MediaEvent, MediaEventType, ConnectionId, MediaEvent, MediaEventType,
MediaConfigurationState, NetEventType } from "../awrtc/index"; MediaConfigurationState, NetEventType, BrowserMediaStream } from "../awrtc/index";
export class MediaNetworkTest{ export class MediaNetworkTest{
...@@ -77,7 +77,8 @@ export class MediaNetworkTest{ ...@@ -77,7 +77,8 @@ export class MediaNetworkTest{
}); });
it("MediaEvent", (done) => { it("MediaEventLocal", (done) => {
BrowserMediaStream.DEBUG_SHOW_ELEMENTS = true;
let mediaConfig = new MediaConfig(); let mediaConfig = new MediaConfig();
let network = this.createDefault(); let network = this.createDefault();
...@@ -88,6 +89,7 @@ export class MediaNetworkTest{ ...@@ -88,6 +89,7 @@ export class MediaNetworkTest{
let evt : MediaEvent = null; let evt : MediaEvent = null;
while((evt = network.DequeueMediaEvent()) != null) while((evt = network.DequeueMediaEvent()) != null)
{ {
console.log("Stream added",evt );
expect(evt.EventType).toBe(MediaEventType.StreamAdded); expect(evt.EventType).toBe(MediaEventType.StreamAdded);
expect(evt.Args.videoHeight).toBeGreaterThan(0); expect(evt.Args.videoHeight).toBeGreaterThan(0);
expect(evt.Args.videoWidth).toBeGreaterThan(0); expect(evt.Args.videoWidth).toBeGreaterThan(0);
...@@ -100,7 +102,7 @@ export class MediaNetworkTest{ ...@@ -100,7 +102,7 @@ export class MediaNetworkTest{
it("MediaEventRemote", (done) => { it("MediaEventRemote", (done) => {
BrowserMediaStream.DEBUG_SHOW_ELEMENTS = true;
let testaddress = "testaddress" + Math.random(); let testaddress = "testaddress" + Math.random();
let sender = this.createDefault(); let sender = this.createDefault();
let receiver = this.createDefault(); let receiver = this.createDefault();
...@@ -167,9 +169,9 @@ export class MediaNetworkTest{ ...@@ -167,9 +169,9 @@ export class MediaNetworkTest{
if(senderFrame && receiverFrame) if(senderFrame && receiverFrame)
done(); done();
}, 10); }, 40);
}); }, 15000);
} }
} }
......
import { VideoInput, Media, DeviceApi, MediaConfig, CAPI_DeviceApi_Devices_Length, CAPI_DeviceApi_Devices_Get } from "../awrtc/index";
import { MakeTestCanvas } from "VideoInputTest";
export function MediaTest_export()
{
}
describe("MediaTest", () => {
beforeEach((done)=>{
let handler = ()=>{
DeviceApi.RemOnChangedHandler(handler);
done();
};
DeviceApi.AddOnChangedHandler(handler);
DeviceApi.Update();
Media.ResetSharedInstance();
});
it("SharedInstance", () => {
expect(Media.SharedInstance).toBeTruthy();
let instance1 = Media.SharedInstance;
Media.ResetSharedInstance();
expect(Media.SharedInstance).not.toBe(instance1);
});
it("GetVideoDevices", () => {
const media = new Media();
let devs = media.GetVideoDevices();
expect(devs).toBeTruthy();
expect(devs.length).toBeGreaterThan(0);
});
it("GetUserMedia", async () => {
const media = new Media();
let config = new MediaConfig();
config.Audio = false;
let stream = await media.getUserMedia(config);
expect(stream).not.toBeNull();
expect(stream.getAudioTracks().length).toBe(0);
expect(stream.getVideoTracks().length).toBe(1);
stream = null;
let err = null;
config.VideoDeviceName = "invalid name"
console.log("Expecting error message: Failed to find deviceId for label invalid name");
try{
stream = await media.getUserMedia(config);
}catch(error){
err = error;
}
expect(err).not.toBeNull();
expect(stream).toBeNull();
});
it("GetUserMedia_videoinput", async (done) => {
const name = "test_canvas";
const media = new Media();
const config = new MediaConfig();
config.Audio = false;
config.Video = true;
const canvas = MakeTestCanvas();
media.VideoInput.AddCanvasDevice(name, canvas);
const streamCamera = await media.getUserMedia(config);
expect(streamCamera).not.toBeNull();
expect(streamCamera.getAudioTracks().length).toBe(0);
expect(streamCamera.getVideoTracks().length).toBe(1);
config.VideoDeviceName = name;
const streamCanvas = await media.getUserMedia(config);
expect(streamCanvas).not.toBeNull();
expect(streamCanvas.getAudioTracks().length).toBe(0);
expect(streamCanvas.getVideoTracks().length).toBe(1);
const streamCanvas2 = await media.getUserMedia(config);
expect(streamCanvas2).not.toBeNull();
expect(streamCanvas2.getAudioTracks().length).toBe(0);
expect(streamCanvas2.getVideoTracks().length).toBe(1);
done();
});
//CAPI needs to be changed to use Media only instead the device API
it("MediaCapiVideoInput", async (done) => {
//empty normal device api
DeviceApi.Reset();
expect(CAPI_DeviceApi_Devices_Length()).toBe(0);
const name = "test_canvas";
const canvas = MakeTestCanvas();
Media.SharedInstance.VideoInput.AddCanvasDevice(name, canvas);
expect(CAPI_DeviceApi_Devices_Length()).toBe(1);
expect(CAPI_DeviceApi_Devices_Get(0)).toBe(name);
done();
});
});
\ No newline at end of file
import { VideoInput } from "../awrtc/index";
export function VideoInputTest_export()
{
}
export function MakeTestCanvas() : HTMLCanvasElement{
let canvas = document.createElement("canvas");
let ctx = canvas.getContext("2d");
//make blue for debugging purposes
ctx.fillStyle = "blue";
ctx.fillRect(0, 0, canvas.width, canvas.height);
return canvas;
}
export function MakeBrokenTestCanvas() : HTMLCanvasElement{
let canvas = document.createElement("canvas");
return canvas;
}
describe("VideoInputTest", () => {
beforeEach(()=>{
});
it("AddRem", () => {
let name = "test_canvas";
let vi = new VideoInput();
let canvas = document.createElement("canvas")
expect(vi.HasDevice(name)).toBe(false);
vi.AddCanvasDevice(name, canvas);
expect(vi.HasDevice(name)).toBe(true);
vi.RemCanvasDevice(name);
expect(vi.HasDevice(name)).toBe(false);
});
it("GetDeviceNames", () => {
let name = "test_canvas";
let name2 = "test_canvas2";
let vi = new VideoInput();
let canvas = document.createElement("canvas")
let names = vi.GetDeviceNames();
expect(names).toBeTruthy();
expect(names.length).toBe(0);
vi.AddCanvasDevice(name, canvas);
names = vi.GetDeviceNames();
expect(names).toBeTruthy();
expect(names.length).toBe(1);
expect(names[0]).toBe(name);
vi.AddCanvasDevice(name, canvas);
names = vi.GetDeviceNames();
expect(names).toBeTruthy();
expect(names.length).toBe(1);
expect(names[0]).toBe(name);
vi.AddCanvasDevice(name2, canvas);
names = vi.GetDeviceNames();
expect(names).toBeTruthy();
expect(names.length).toBe(2);
expect(names.sort()).toEqual([name, name2].sort());
});
it("GetStream", () => {
let name = "test_canvas";
let vi = new VideoInput();
let canvas = MakeTestCanvas();
let stream = vi.GetStream(name);
expect(stream).toBeNull();
vi.AddCanvasDevice(name, canvas);
stream = vi.GetStream(name);
expect(stream).toBeTruthy();
});
//not yet clear how this can be handled
//this test will trigger an error in firefox
it("GetStream_no_context", () => {
let name = "test_canvas";
let vi = new VideoInput();
let canvas = MakeBrokenTestCanvas();
//let ctx = canvas.getContext("2d");
let stream = vi.GetStream(name);
expect(stream).toBeNull();
vi.AddCanvasDevice(name, canvas);
stream = vi.GetStream(name);
expect(stream).toBeTruthy();
});
});
\ No newline at end of file
...@@ -34,3 +34,6 @@ export * from "./CallTest" ...@@ -34,3 +34,6 @@ export * from "./CallTest"
export * from "./MediaNetworkTest" export * from "./MediaNetworkTest"
export * from "./BrowserApiTest" export * from "./BrowserApiTest"
export * from "./DeviceApiTest" export * from "./DeviceApiTest"
export * from "./VideoInputTest"
export * from "./MediaTest"
...@@ -21,6 +21,8 @@ ...@@ -21,6 +21,8 @@
"LocalNetworkTest.ts", "LocalNetworkTest.ts",
"MediaNetworkTest.ts", "MediaNetworkTest.ts",
"DeviceApiTest.ts", "DeviceApiTest.ts",
"BrowserApiTest.ts" "VideoInputTest.ts",
"BrowserApiTest.ts",
"MediaTest.ts"
] ]
} }
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment