*  Add signaling web server

add signaling web server

* renamed cmd

* Updated WebApp

* updated build pipeline

* Added C# signaling script

* removed unused file
This commit is contained in:
Kazuki Matsumoto 2019-05-28 00:12:30 +09:00 коммит произвёл flame99999
Родитель 2974180fcf
Коммит 1917e965b5
34 изменённых файлов: 8647 добавлений и 27 удалений

Просмотреть файл

@ -30,6 +30,7 @@ pack:
dependencies:
{% for platform in platforms %}
- .yamato/upm-ci-webrtc.yml#build_{{ platform.name }}
- .yamato/upm-ci-webapp.yml#pack_{{ platform.name }}
{% endfor %}
{% for editor in editors %}

47
.yamato/upm-ci-webapp.yml Normal file
Просмотреть файл

@ -0,0 +1,47 @@
platforms:
- name: win
type: Unity::VM
image: package-ci/win10:stable
flavor: m1.xlarge
pack_command: pack_webapp.cmd
test_command: test_webapp.cmd
projects:
- packagename: com.unity.webapp.renderstreaming
---
{% for project in projects %}
{% for platform in platforms %}
pack_{{ platform.name }}:
name : Pack {{ project.packagename }} on {{ platform.name }}
agent:
type: {{ platform.type }}
image: {{ platform.image }}
flavor: {{ platform.flavor}}
commands:
- {{ platform.pack_command }}
artifacts:
packages:
paths:
- "Assets/bin~/**/*"
{% endfor %}
{% for platform in platforms %}
test_{{ platform.name }}:
name : Test {{ project.packagename }} on {{ platform.name }}
agent:
type: {{ platform.type }}
image: {{ platform.image }}
flavor: {{ platform.flavor}}
commands:
- {{ platform.test_command }}
artifacts:
logs:
paths:
- "WebApp/output.log"
- "WebApp/coverage/**/*"
dependencies:
{% for platform in platforms %}
- .yamato/upm-ci-webapp.yml#pack_{{ platform.name }}
{% endfor %}
{% endfor %}
{% endfor %}

Просмотреть файл

@ -10,7 +10,7 @@ platforms:
# image: package-ci/win10:stable
image: renderstreaming/win10:latest
flavor: m1.large
build_command: build.cmd
build_command: build_plugin.cmd
projects:
- name: webrtc
packagename: com.unity.webrtc
@ -98,7 +98,7 @@ publish:
dependencies:
- .yamato/upm-ci-webrtc.yml#pack
{% for editor in test_editors %}
{% for platform in test_platforms %}
{% for platform in platforms %}
- .yamato/upm-ci-webrtc.yml#test_{{ platform.name }}_{{ editor.version }}
{% endfor %}
{% endfor %}

Просмотреть файл

@ -38,7 +38,7 @@ RenderSettings:
m_ReflectionIntensity: 1
m_CustomReflection: {fileID: 0}
m_Sun: {fileID: 0}
m_IndirectSpecularColor: {r: 0.44657898, g: 0.4964133, b: 0.5748178, a: 1}
m_IndirectSpecularColor: {r: 0.4465934, g: 0.49642956, b: 0.5748249, a: 1}
m_UseRadianceAmbientProbe: 0
--- !u!157 &3
LightmapSettings:
@ -50,12 +50,11 @@ LightmapSettings:
m_BounceScale: 1
m_IndirectOutputScale: 1
m_AlbedoBoost: 1
m_TemporalCoherenceThreshold: 1
m_EnvironmentLightingMode: 0
m_EnableBakedLightmaps: 1
m_EnableRealtimeLightmaps: 0
m_LightmapEditorSettings:
serializedVersion: 10
serializedVersion: 12
m_Resolution: 2
m_BakeResolution: 10
m_AtlasSize: 512
@ -63,6 +62,7 @@ LightmapSettings:
m_AOMaxDistance: 1
m_CompAOExponent: 1
m_CompAOExponentDirect: 0
m_ExtractAmbientOcclusion: 0
m_Padding: 2
m_LightmapParameters: {fileID: 0}
m_LightmapsBakeMode: 1
@ -77,10 +77,16 @@ LightmapSettings:
m_PVRDirectSampleCount: 32
m_PVRSampleCount: 256
m_PVRBounces: 2
m_PVREnvironmentSampleCount: 256
m_PVREnvironmentReferencePointCount: 2048
m_PVRFilteringMode: 2
m_PVRDenoiserTypeDirect: 0
m_PVRDenoiserTypeIndirect: 0
m_PVRDenoiserTypeAO: 0
m_PVRFilterTypeDirect: 0
m_PVRFilterTypeIndirect: 0
m_PVRFilterTypeAO: 0
m_PVRFilteringMode: 1
m_PVREnvironmentMIS: 0
m_PVRCulling: 1
m_PVRFilteringGaussRadiusDirect: 1
m_PVRFilteringGaussRadiusIndirect: 5
@ -89,6 +95,7 @@ LightmapSettings:
m_PVRFilteringAtrousPositionSigmaIndirect: 2
m_PVRFilteringAtrousPositionSigmaAO: 1
m_ShowResolutionOverlay: 1
m_ExportTrainingData: 0
m_LightingDataAsset: {fileID: 0}
m_UseShadowmask: 1
--- !u!196 &4
@ -113,11 +120,59 @@ NavMeshSettings:
debug:
m_Flags: 0
m_NavMeshData: {fileID: 0}
--- !u!1 &8165320
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 8165321}
- component: {fileID: 8165322}
m_Layer: 0
m_Name: Render Streaming
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!4 &8165321
Transform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 8165320}
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_Children: []
m_Father: {fileID: 0}
m_RootOrder: 2
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!114 &8165322
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 8165320}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 045786cf504bd7347842d6948241cbd0, type: 3}
m_Name:
m_EditorClassIdentifier:
urlSignaling: http://localhost
urlSTUN: stun:stun.l.google.com:19302
interval: 0
text: {fileID: 0}
--- !u!1 &170076733
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInternal: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 170076735}
@ -133,15 +188,17 @@ GameObject:
Light:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInternal: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 170076733}
m_Enabled: 1
serializedVersion: 8
serializedVersion: 9
m_Type: 1
m_Color: {r: 1, g: 0.95686275, b: 0.8392157, a: 1}
m_Intensity: 1
m_Range: 10
m_SpotAngle: 30
m_InnerSpotAngle: 21.80208
m_CookieSize: 10
m_Shadows:
m_Type: 2
@ -151,6 +208,24 @@ Light:
m_Bias: 0.05
m_NormalBias: 0.4
m_NearPlane: 0.2
m_CullingMatrixOverride:
e00: 1
e01: 0
e02: 0
e03: 0
e10: 0
e11: 1
e12: 0
e13: 0
e20: 0
e21: 0
e22: 1
e23: 0
e30: 0
e31: 0
e32: 0
e33: 1
m_UseCullingMatrixOverride: 0
m_Cookie: {fileID: 0}
m_DrawHalo: 0
m_Flare: {fileID: 0}
@ -158,19 +233,23 @@ Light:
m_CullingMask:
serializedVersion: 2
m_Bits: 4294967295
m_RenderingLayerMask: 1
m_Lightmapping: 1
m_LightShadowCasterMode: 0
m_AreaSize: {x: 1, y: 1}
m_BounceIntensity: 1
m_ColorTemperature: 6570
m_UseColorTemperature: 0
m_BoundingSphereOverride: {x: 0, y: 0, z: 0, w: 0}
m_UseBoundingSphereOverride: 0
m_ShadowRadius: 0
m_ShadowAngle: 0
--- !u!4 &170076735
Transform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInternal: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 170076733}
m_LocalRotation: {x: 0.40821788, y: -0.23456968, z: 0.10938163, w: 0.8754261}
m_LocalPosition: {x: 0, y: 3, z: 0}
@ -183,7 +262,8 @@ Transform:
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInternal: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 282840814}
@ -200,20 +280,24 @@ GameObject:
AudioListener:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInternal: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 282840810}
m_Enabled: 1
--- !u!20 &282840813
Camera:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInternal: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 282840810}
m_Enabled: 1
serializedVersion: 2
m_ClearFlags: 1
m_ClearFlags: 2
m_BackGroundColor: {r: 0.19215687, g: 0.3019608, b: 0.4745098, a: 0}
m_projectionMatrixMode: 1
m_GateFitMode: 2
m_FOVAxisMode: 0
m_SensorSize: {x: 36, y: 24}
m_LensShift: {x: 0, y: 0}
m_FocalLength: 50
@ -247,7 +331,8 @@ Camera:
Transform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInternal: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 282840810}
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 1, z: -10}
@ -256,3 +341,69 @@ Transform:
m_Father: {fileID: 0}
m_RootOrder: 0
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!1 &1632470018
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 1632470021}
- component: {fileID: 1632470020}
- component: {fileID: 1632470019}
m_Layer: 0
m_Name: EventSystem
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!114 &1632470019
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1632470018}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 1077351063, guid: f70555f144d8491a825f0804e09c671c, type: 3}
m_Name:
m_EditorClassIdentifier:
m_HorizontalAxis: Horizontal
m_VerticalAxis: Vertical
m_SubmitButton: Submit
m_CancelButton: Cancel
m_InputActionsPerSecond: 10
m_RepeatDelay: 0.5
m_ForceModuleActive: 0
--- !u!114 &1632470020
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1632470018}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: -619905303, guid: f70555f144d8491a825f0804e09c671c, type: 3}
m_Name:
m_EditorClassIdentifier:
m_FirstSelected: {fileID: 0}
m_sendNavigationEvents: 1
m_DragThreshold: 10
--- !u!4 &1632470021
Transform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1632470018}
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_Children: []
m_Father: {fileID: 0}
m_RootOrder: 3
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}

8
Assets/Scripts.meta Normal file
Просмотреть файл

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: cc7ace171347147499aba2a6042b3285
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

Просмотреть файл

@ -0,0 +1,161 @@
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Networking;
using UnityEngine.UI;
using Unity.WebRTC;
namespace Unity.RenderStreaming
{
public class RenderStreaming : MonoBehaviour
{
[SerializeField]
private string urlSignaling = "http://localhost";
[SerializeField]
private string urlSTUN = "stun:stun.l.google.com:19302";
[SerializeField]
private float interval = 5.0f;
private Signaling signaling;
private Dictionary<string, RTCPeerConnection> pcs = new Dictionary<string, RTCPeerConnection>();
private RTCConfiguration conf;
private string sessionId;
public void Awake()
{
WebRTC.WebRTC.Initialize();
}
public void OnDestroy()
{
WebRTC.WebRTC.Finalize();
}
public IEnumerator Start()
{
signaling = new Signaling(urlSignaling);
var opCreate = signaling.Create();
yield return opCreate;
if (opCreate.webRequest.isNetworkError)
{
Debug.LogError($"Network Error: {opCreate.webRequest.error}");
yield break;
}
var newResData = opCreate.webRequest.DownloadHandlerJson<NewResData>().GetObject();
sessionId = newResData.sessionId;
conf = default;
conf.iceServers = new RTCIceServer[]
{
new RTCIceServer { urls = new string[] { urlSTUN } }
};
StartCoroutine(LoopPolling());
}
IEnumerator LoopPolling()
{
while (true)
{
yield return StartCoroutine(GetOffer());
yield return StartCoroutine(GetCandidate());
yield return new WaitForSeconds(interval);
}
}
IEnumerator GetOffer()
{
var op = signaling.GetOffer(sessionId);
yield return op;
if (op.webRequest.isNetworkError)
{
Debug.LogError($"Network Error: {op.webRequest.error}");
yield break;
}
var obj = op.webRequest.DownloadHandlerJson<OfferListResData>().GetObject();
foreach (var offer in obj.offers)
{
RTCSessionDescription _desc = default;
_desc.type = RTCSdpType.Offer;
_desc.sdp = offer.sdp;
var connectionId = offer.connectionId;
if(pcs.ContainsKey(connectionId))
{
continue;
}
var pc = new RTCPeerConnection();
pcs.Add(offer.connectionId, pc);
pc.SetConfiguration(ref conf);
pc.onIceCandidate = delegate (ref RTCIceCandidate candidate) { StartCoroutine(OnIceCandidate(connectionId, candidate)); };
pc.SetRemoteDescription(ref _desc);
StartCoroutine(Answer(connectionId));
}
}
IEnumerator Answer(string connectionId)
{
RTCAnswerOptions options = default;
var pc = pcs[connectionId];
var op = pc.CreateAnswer(ref options);
yield return op;
if (op.isError)
{
Debug.LogError($"Network Error: {op.error}");
yield break;
}
var opLocalDesc = pc.SetLocalDescription(ref op.desc);
yield return opLocalDesc;
if (opLocalDesc.isError)
{
Debug.LogError($"Network Error: {opLocalDesc.error}");
yield break;
}
var op3 = signaling.PostAnswer(this.sessionId, connectionId, op.desc.sdp);
yield return op3;
if (op3.webRequest.isNetworkError)
{
Debug.LogError($"Network Error: {op3.webRequest.error}");
yield break;
}
}
IEnumerator GetCandidate()
{
var op = signaling.GetCandidate(sessionId);
yield return op;
if (op.webRequest.isNetworkError)
{
Debug.LogError($"Network Error: {op.webRequest.error}");
yield break;
}
var obj = op.webRequest.DownloadHandlerJson<CandidateListResData>().GetObject();
foreach (var candidate in obj.candidates)
{
if (!pcs.ContainsKey(candidate.connectionId))
{
continue;
}
RTCIceCandidate _candidate = default;
_candidate.candidate = candidate.candidate;
pcs[candidate.connectionId].AddIceCandidate(ref _candidate);
}
}
IEnumerator OnIceCandidate(string connectionId, RTCIceCandidate candidate)
{
var opCandidate = signaling.PostCandidate(sessionId, connectionId, candidate.candidate);
yield return opCandidate;
if (opCandidate.webRequest.isNetworkError)
{
Debug.LogError($"Network Error: {opCandidate.webRequest.error}");
yield break;
}
}
}
}

Просмотреть файл

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 045786cf504bd7347842d6948241cbd0
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

208
Assets/Scripts/Signaling.cs Normal file
Просмотреть файл

@ -0,0 +1,208 @@
using System;
using UnityEngine;
using UnityEngine.Networking;
namespace Unity.RenderStreaming
{
public class DownloadHandlerJson<T> : DownloadHandlerScript
{
private T m_obj;
public DownloadHandlerJson() : base()
{
}
public DownloadHandlerJson(byte[] buffer) : base(buffer)
{
}
protected override byte[] GetData() { return null; }
protected override bool ReceiveData(byte[] data, int dataLength)
{
if (data == null || data.Length < 1)
{
return false;
}
var text = System.Text.Encoding.UTF8.GetString(data);
try
{
m_obj = JsonUtility.FromJson<T>(text);
}
catch(Exception e)
{
Debug.LogError(text);
throw e;
}
return true;
}
public T GetObject()
{
return m_obj;
}
}
static class DownloadHandlerExtension
{
public static T FromJson<T>(this DownloadHandler handler)
{
return JsonUtility.FromJson<T>(handler.text);
}
}
static class UnityWebRequestExtension
{
public static UnityWebRequestAsyncOperation SendWebRequest<T>(this UnityWebRequest own)
{
own.downloadHandler = new DownloadHandlerJson<T>();
var req = own.SendWebRequest();
return req;
}
public static DownloadHandlerJson<T> DownloadHandlerJson<T>(this UnityWebRequest own)
{
return own.downloadHandler as DownloadHandlerJson<T>;
}
}
#pragma warning disable 0649
[Serializable]
class NewResData
{
public string sessionId;
}
[Serializable]
class OfferListResData
{
public OfferResData[] offers;
}
[Serializable]
class CandidateListResData
{
public CandidateResData[] candidates;
}
[Serializable]
class OfferResData
{
public string connectionId;
public string sdp;
}
[Serializable]
class AnswerResData
{
public string connectionId;
public string sdp;
}
[Serializable]
class CandidateResData
{
public string connectionId;
public string candidate;
}
#pragma warning restore 0649
public class Signaling
{
public string Url { get; }
public Signaling(string url)
{
Url = url;
}
[Serializable]
class OfferReqData
{
public string connectionId;
public string sdp;
}
[Serializable]
class AnswerReqData
{
public string connectionId;
public string sdp;
}
[Serializable]
class CandidateReqData
{
public string connectionId;
public string candidate;
}
public UnityWebRequestAsyncOperation Create()
{
var req = new UnityWebRequest($"{Url}/signaling", "PUT");
var op = req.SendWebRequest<NewResData>();
return op;
}
public UnityWebRequestAsyncOperation Delete()
{
var req = new UnityWebRequest($"{Url}/signaling", "DELETE");
var op = req.SendWebRequest();
return op;
}
public UnityWebRequestAsyncOperation PostOffer(string sessionId, string connectionId, string sdp)
{
var obj = new OfferReqData { connectionId = connectionId, sdp = sdp };
var data = new System.Text.UTF8Encoding().GetBytes(JsonUtility.ToJson(obj));
var req = new UnityWebRequest($"{Url}/signaling/offer", "POST");
req.SetRequestHeader("Session-Id", sessionId);
req.uploadHandler = new UploadHandlerRaw(data);
var op = req.SendWebRequest();
return op;
}
public UnityWebRequestAsyncOperation GetOffer(string sessionId)
{
var req = new UnityWebRequest($"{Url}/signaling/offer", "GET");
req.SetRequestHeader("Session-Id", sessionId);
var op = req.SendWebRequest<OfferListResData>();
return op;
}
public UnityWebRequestAsyncOperation PostAnswer(string sessionId, string connectionId, string sdp)
{
var obj = new AnswerReqData { connectionId = connectionId, sdp = sdp };
var data = new System.Text.UTF8Encoding().GetBytes(JsonUtility.ToJson(obj));
var req = new UnityWebRequest($"{Url}/signaling/answer", "POST");
req.SetRequestHeader("Session-Id", sessionId);
req.uploadHandler = new UploadHandlerRaw(data);
var op = req.SendWebRequest();
return op;
}
public UnityWebRequestAsyncOperation GetAnswer(string sessionId, string connectionId)
{
var req = new UnityWebRequest($"{Url}/signaling/answer", "GET");
req.SetRequestHeader("Session-Id", sessionId);
var op = req.SendWebRequest<AnswerResData>();
return op;
}
public UnityWebRequestAsyncOperation PostCandidate(string sessionId, string connectionId, string candidate)
{
var obj = new CandidateReqData { connectionId = connectionId, candidate = candidate };
var data = new System.Text.UTF8Encoding().GetBytes(JsonUtility.ToJson(obj));
var req = new UnityWebRequest($"{Url}/signaling/candidate", "POST");
req.SetRequestHeader("Session-Id", sessionId);
req.uploadHandler = new UploadHandlerRaw(data);
var op = req.SendWebRequest();
return op;
}
public UnityWebRequestAsyncOperation GetCandidate(string sessionId)
{
var req = new UnityWebRequest($"{Url}/signaling/candidate", "GET");
req.SetRequestHeader("Session-Id", sessionId);
var op = req.SendWebRequest<CandidateListResData>();
return op;
}
}
}

Просмотреть файл

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 6ee3c31b4dd467b47a7050eff711f2c3
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

21
WebApp/.editorconfig Normal file
Просмотреть файл

@ -0,0 +1,21 @@
# References
# https://github.com/MicrosoftDocs/visualstudio-docs/blob/master/docs/ide/editorconfig-code-style-settings-reference.md#example-editorconfig-file
###############################
# Core EditorConfig Options #
###############################
root = true
# All files
[*]
indent_style = space
# Code files
[*.{ts,js,json,html}]
end_of_line = crlf
indent_size = 2
indent_style = space
insert_final_newline = true
charset = utf-8-bom
trim_trailing_whitespace = true

11
WebApp/index.html Normal file
Просмотреть файл

@ -0,0 +1,11 @@
<!DOCTYPE HTML>
<html>
<head>
<link rel="icon" href="/public/images/favicon.ico" type="image/x-icon">
<link type="text/css" rel="stylesheet" href="style.css">
<script type="text/javascript" src="https://webrtc.github.io/adapter/adapter-latest.js"></script>
<script type="module" src="app.js"></script>
</head>
<div id="player"></div>
</body>
</html>

10
WebApp/jest.config.js Normal file
Просмотреть файл

@ -0,0 +1,10 @@
module.exports = {
roots: ['<rootDir>/src/', '<rootDir>/test/'],
transform: {
'^.+\\.tsx?$': 'ts-jest'
},
testRegex: '(/__tests__/.*|\\.(test|spec))\\.[tj]sx?$',
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'],
coverageDirectory: './coverage/',
collectCoverage: true
}

6819
WebApp/package-lock.json сгенерированный Normal file

Разница между файлами не показана из-за своего большого размера Загрузить разницу

Просмотреть файл

@ -1,14 +1,41 @@
{
"name": "WebApp",
"version": "0.0.1",
"name": "signalingwebserver",
"version": "0.0.0",
"private": true,
"scripts": {
"prestart": "npm install",
"build": "tsc -p tsconfig.json",
"test": "jest --env=node --colors --coverage test",
"newman": "newman run test/renderstreaming.postman_collection.json",
"start": "node ./build/index.js",
"dev": "ts-node ./src/index.ts",
"lint": "tslint --project tsconfig.json --fix 'src/*.ts'",
"pack": "pkg ."
},
"dependencies": {
"cookie-parser": "~1.4.3",
"debug": "~2.6.9",
"@types/express": "^4.16.1",
"@types/node": "^11.12.0",
"express": "~4.16.0",
"http-errors": "~1.6.2",
"jade": "~1.11.0",
"morgan": "~1.9.0",
"socket.io": "~2.0.4"
"debug": "~2.6.9"
},
"devDependencies": {
"@types/jest": "^24.0.12",
"jest": "^24.8.0",
"newman": "^4.4.1",
"pkg": "^4.4.0",
"ts-jest": "^24.0.2",
"ts-node": "^8.1.0",
"tslint": "^5.14.0",
"tslint-config-airbnb": "^5.11.1",
"typescript": "^3.3.4000"
},
"bin": "build/index.js",
"pkg": {
"assets": [
"public/**/*"
],
"targets": [
"node10"
]
}
}

Двоичные данные
WebApp/public/images/Play.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 472 KiB

Двоичные данные
WebApp/public/images/favicon.ico Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 66 KiB

Просмотреть файл

@ -0,0 +1,40 @@
import { VideoPlayer } from "./video-player.js";
import { registerKeyboardEvents, registerMouseEvents } from "./register-events.js";
let playButton;
showPlayButton();
function showPlayButton() {
if (!document.getElementById('playButton')) {
let playButtonImg = document.createElement('img');
playButtonImg.id = 'playButton';
playButtonImg.src = 'images/Play.png';
playButtonImg.alt = 'Start Streaming';
if (!playButton) {
playButton = document.getElementById('player').appendChild(playButtonImg);
}
playButton.addEventListener('click', function () {
onClickPlayButton();
playButton.style.display = 'none';
});
}
else {
playButton.style.display = 'block';
}
}
function onClickPlayButton() {
const playerDiv = document.getElementById('player');
const element = document.createElement('video');
playerDiv.appendChild(element);
const videoPlayer = setupVideoPlayer(element);
registerKeyboardEvents(videoPlayer);
registerMouseEvents(videoPlayer, element);
}
function setupVideoPlayer(element, clientConfig) {
let videoPlayer = new VideoPlayer(element, clientConfig);
videoPlayer.setupConnection();
return videoPlayer;
}

Просмотреть файл

@ -0,0 +1,100 @@
let InputEvent = {
KeyDown : 0,
KeyUp : 1,
MouseDown : 2,
MouseUp : 3,
MouseMove : 4,
MouseWheel : 5
};
let isPlayMode = false;
export function registerKeyboardEvents(videoPlayer) {
const _videoPlayer = videoPlayer;
document.addEventListener('keydown', function (e) {
console.log("key down " + e.key + ", repeat = " + e.repeat);
_videoPlayer && _videoPlayer.sendMsg(new Uint8Array([InputEvent.KeyDown, e.key]).buffer);
}, false);
document.addEventListener('keyup', function (e) {
console.log("key up " + e.key);
_videoPlayer && _videoPlayer.sendMsg(new Uint8Array([InputEvent.KeyUp, e.key]).buffer);
}, false);
}
export function registerMouseEvents(videoPlayer, playerElement) {
const _videoPlayer = videoPlayer;
const _playerElement = playerElement;
const _document = document;
playerElement.requestPointerLock = playerElement.requestPointerLock ||
playerElement.mozRequestPointerLock || playerElement.webkitRequestPointerLock;
// Listen to lock state change events
document.addEventListener('pointerlockchange', pointerLockChange, false);
document.addEventListener('mozpointerlockchange', pointerLockChange, false);
document.addEventListener('webkitpointerlockchange', pointerLockChange, false);
// Listen to mouse events
playerElement.addEventListener('click', playVideo, false);
playerElement.addEventListener('mousedown', sendMouseDown, false);
playerElement.addEventListener('mouseup', sendMouseUp, false);
playerElement.addEventListener('mousewheel', sendMouseWheel, false);
// ios workaround for not allowing auto-play
playerElement.addEventListener('touchend', playVideoWithTouch , false);
function pointerLockChange() {
if (_document.pointerLockElement === playerElement ||
_document.mozPointerLockElement === playerElement ||
_document.webkitPointerLockElement === playerElement) {
isPlayMode = false;
console.log('Pointer locked');
document.addEventListener('mousemove', sendMousePosition, false);
}
else {
console.log('The pointer lock status is now unlocked');
document.removeEventListener('mousemove', sendMousePosition, false);
}
}
function playVideo() {
if (_playerElement.paused) {
_playerElement.play();
}
if (!isPlayMode) {
_playerElement.requestPointerLock();
isPlayMode = true;
}
}
function playVideoWithTouch() {
if (_playerElement.paused) {
_playerElement.play();
}
}
function sendMousePosition(e) {
console.log("deltaX: " + e.movementX + ", deltaY: " + e.movementY);
let data = new DataView(new ArrayBuffer(5));
data.setUint8(0, InputEvent.MouseMove);
data.setInt16(1, e.movementX, true);
data.setInt16(3, e.movementY, true);
_videoPlayer.sendMsg(data.buffer);
}
function sendMouseDown(e) {
console.log("mouse button " + e.button + " down");
let data = new DataView(new ArrayBuffer(2));
data.setUint8(0, InputEvent.MouseDown);
data.setUint8(1, e.button);
_videoPlayer && _videoPlayer.sendMsg(data.buffer);
}
function sendMouseUp(e) {
console.log("mouse button " + e.button + " up");
let data = new DataView(new ArrayBuffer(2));
data.setUint8(0, InputEvent.MouseUp);
data.setUint8(1, e.button);
_videoPlayer && _videoPlayer.sendMsg(data.buffer);
}
function sendMouseWheel(e) {
console.log("mouse wheel with delta " + e.wheelDelta);
let data = new DataView(new ArrayBuffer(3));
data.setUint8(0, InputEvent.MouseWheel);
data.setInt16(1, e.wheelDelta, true);
_videoPlayer && _videoPlayer.sendMsg(data.buffer);
}
}

Просмотреть файл

@ -0,0 +1,56 @@
export default class SignalingChannel {
headers(sessionId) {
if(sessionId != undefined)
{
return {'Content-Type': 'application/json', 'Session-Id': sessionId};
}
else {
return {'Content-Type': 'application/json'};
}
};
url(method) {
return location.protocol + '//' + location.host + '/signaling/' + method;
};
async send(sessionId, data) {
let method = undefined;
if ('type' in data) {
switch (data.type) {
case 'offer':
method = 'offer';
break;
case 'answer':
method = 'answer';
break;
default:
return;
}
} else {
method = 'candidate';
}
const response = await fetch(this.url(method), {method: 'POST', headers: this.headers(sessionId), body: JSON.stringify(data)});
return await response.json();
};
async create() {
const response = await fetch(this.url(''), {method: 'PUT', headers: this.headers()});
return await response.json();
};
async delete(sessionId) {
await fetch(this.url(''), {method: 'DELETE', headers: this.headers(sessionId)});
return;
};
async getOffer(sessionId) {
const response = await fetch(this.url('offer'), {method: 'GET', headers: this.headers(sessionId)});
return await response.json();
};
async getAnswer(sessionId) {
const response = await fetch(this.url('answer'), {method: 'GET', headers: this.headers(sessionId)});
return await response.json();
};
async getCandidate(sessionId) {
const response = await fetch(this.url('candidate'), {method: 'GET', headers: this.headers(sessionId)});
return await response.json();
};
}

Просмотреть файл

@ -0,0 +1,145 @@
import SignalingChannel from "./signaling-channel.js"
export class VideoPlayer {
constructor(element, options) {
const _this = this;
if(options == undefined) {
options = {};
}
this.cfg = options;
this.cfg.sdpSemantics = 'unified-plan';
this.cfg.iceServers = [{urls: ['stun:stun.l.google.com:19302']}];
this.pc = null;
this.channel = null;
this.offerOptions = {
offerToReceiveAudio: true,
offerToReceiveVideo: true,
};
this.video = element;
this.video.id = 'Video';
this.video.playsInline = true;
this.video.addEventListener('loadedmetadata', function () {
_this.video.play();
}, true);
this.interval = 5000;
this.signalingChannel = new SignalingChannel();
this.sleep = msec => new Promise(resolve => setTimeout(resolve, msec));
}
async setupConnection() {
var _this = this;
// close current RTCPeerConnection
if (this.pc) {
console.log('Close current PeerConnection');
this.pc.close();
this.pc = null;
}
// Create peerConnection with proxy server and set up handlers
this.pc = new RTCPeerConnection(this.cfg);
this.pc.onsignalingstatechange = function (e) {
console.log('signalingState changed:', e);
};
this.pc.oniceconnectionstatechange = function (e) {
console.log('iceConnectionState changed:', e);
};
this.pc.onicegatheringstatechange = function (e) {
console.log('iceGatheringState changed:', e);
};
this.pc.ontrack = function (e) {
console.log('New track added: ', e.streams);
_this.video.srcObject = e.streams[0];
};
this.pc.onicecandidate = function (e) {
console.log('Send ICE candidate', e);
_this.signalingChannel.send(_this.sessionId, e);
};
// Create data channel with proxy server and set up handlers
this.channel = this.pc.createDataChannel('ProxyDataChannel');
console.log('Create datachannel.');
this.channel.onopen = function () {
console.log('Datachannel connected.');
};
this.channel.onerror = function (e) {
console.log("The error " + e.error.message + " occurred\n while handling data with proxy server.");
};
this.channel.onclose = function () {
console.log('Datachannel disconnected.');
};
const createResponse = await this.signalingChannel.create();
this.sessionId = createResponse.sessionId;
// create offer
const offer = await this.pc.createOffer(this.offerOptions);
// set local sdp
offer.sdp = offer.sdp.replace(/useinbandfec=1/, 'useinbandfec=1;stereo=1;maxaveragebitrate=1048576');
const desc = new RTCSessionDescription({sdp:offer.sdp, type:"offer"});
await this.pc.setLocalDescription(desc);
await this.sendOffer(offer);
this.loopGetAnswer(this.sessionId, this.interval);
this.loopGetCandidate(this.sessionId, this.interval);
};
async sendOffer(offer) {
// signaling
const res = await this.signalingChannel.send(this.sessionId, offer);
this.connectionId = res.connectionId;
}
async loopGetAnswer(sessionId, interval) {
while(true) {
const res = await this.signalingChannel.getAnswer(sessionId);
if(res.answers.length > 0) {
const answer = res.answers[0];
await this.setAnswer(sessionId, answer.sdp);
}
await this.sleep(interval);
}
}
async loopGetCandidate(sessionId, interval) {
while(true) {
const res = await this.signalingChannel.getCandidate(sessionId);
if(res.candidates.length > 0) {
for(let candidate of res.candidates) {
const iceCandidate = new RTCIceCandidate({ candidate: candidate.candidate });
await this.pc.addIceCandidate(iceCandidate);
}
}
await this.sleep(interval);
}
}
async setAnswer(sessionId, sdp) {
const desc = new RTCSessionDescription({sdp:sdp, type:"answer"});
await this.pc.setRemoteDescription(desc);
}
close() {
if (this.pc) {
console.log('Close current PeerConnection');
this.pc.close();
this.pc = null;
}
};
sendMsg(msg) {
switch (this.channel.readyState) {
case 'connecting':
console.log('Connection not ready');
break;
case 'open':
this.channel.send(msg);
break;
case 'closing':
console.log('Attempt to sendMsg message while closing');
break;
case 'closed':
console.log( 'Attempt to sendMsg message while connection closed.');
break;
}
};
}

Просмотреть файл

@ -0,0 +1,29 @@
body{
margin: 0px;
}
#player{
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
align-items: center;
justify-content: center;
display: flex;
background-color: #323232;
}
#playButton{
width: 15%;
max-width: 200px;
cursor: pointer;
}
#Video{
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}

5
WebApp/run.bat Normal file
Просмотреть файл

@ -0,0 +1,5 @@
@echo off
pushd %~dp0
call npm run start
popd
pause

38
WebApp/src/index.ts Normal file
Просмотреть файл

@ -0,0 +1,38 @@
import * as express from 'express';
import { Server } from 'http';
import { createServer } from './server';
export interface Options {
port?: number;
}
export class RenderStreaming {
public static run(argv: string[]) {
const program = require('commander');
const readOptions = (): Options => {
if (Array.isArray(argv)) {
program
.usage('[options] <apps...>')
.option('-p, --port <n>', 'Port to start the server on', process.env.PORT || 80)
.parse(argv);
return {
port: program.port,
};
}
};
const options = readOptions();
return new RenderStreaming(options);
}
public server: express.Application;
public httpServer?: Server;
public options: Options;
constructor(options: Options) {
this.options = options;
this.server = createServer();
this.httpServer = this.server.listen(this.options.port);
}
}
RenderStreaming.run(process.argv);

27
WebApp/src/log.ts Normal file
Просмотреть файл

@ -0,0 +1,27 @@
const isDebug: boolean = true;
export enum LogLevel {
info,
log,
warn,
error,
}
export function log(level: LogLevel, ...args: any[]): void {
if (isDebug) {
switch (level) {
case LogLevel.log:
console.log(...args);
break;
case LogLevel.info:
console.info(...args);
break;
case LogLevel.warn:
console.warn(...args);
break;
case LogLevel.error:
console.error(...args);
break;
}
}
}

30
WebApp/src/server.ts Normal file
Просмотреть файл

@ -0,0 +1,30 @@
import * as express from 'express';
import * as bodyParser from 'body-parser';
import * as path from 'path';
import * as fs from 'fs';
import signaling from './signaling';
import { log, LogLevel } from './log';
export const createServer = () => {
const app: express.Application = express();
// const signal = require('./signaling');
app.use(bodyParser.urlencoded({ extended: true }));
app.use(bodyParser.json());
app.use('/signaling', signaling);
app.use(express.static(path.join(__dirname, '/../public/stylesheets')));
app.use(express.static(path.join(__dirname, '/../public/scripts')));
app.use('/images', express.static(path.join(__dirname, '/../public/images')));
app.get('/', (req, res) => {
const indexPagePath: string = path.join(__dirname, '/../index.html');
fs.access(indexPagePath, (err) => {
if (err) {
log(LogLevel.warn, `Can't find file ' ${indexPagePath}`);
res.status(404).send(`Can't find file ${indexPagePath}`);
} else {
res.sendFile(indexPagePath);
}
});
});
return app;
};

83
WebApp/src/signaling.ts Normal file
Просмотреть файл

@ -0,0 +1,83 @@
import { Request, Response, Router } from 'express';
import { v4 as uuid } from 'uuid';
const express = require('express');
const router: Router = express.Router();
const clients: Map<string, Set<string>> = new Map<string, Set<string>>();
const offers: Map<string, string> = new Map<string, string>();
const answers: Map<string, string> = new Map<string,string>();
const candidates: Map<string, string> = new Map<string, string>();
router.use((req: Request, res: Response, next) => {
if (req.url == '/') {
next();
return;
}
const id : string = req.header('session-id');
if (!clients.has(id)) {
res.sendStatus(404);
return;
}
next();
});
router.get('/offer', (req: Request, res: Response) => {
const _offers = Array.from(offers);
const obj = _offers.map(v => { return { "connectionId" :v[0], "sdp": v[1], }});
res.json({ offers : obj });
});
router.get('/answer', (req: Request, res: Response) => {
const sessionId : string = req.header('session-id');
let connectionIds = Array.from(clients.get(sessionId));
connectionIds = connectionIds.filter(v => answers.has(v));
const _answers = connectionIds.map(v => { return [v, answers.get(v)] });
const obj = _answers.map(v => { return { "connectionId" :v[0], "sdp": v[1], }});
res.json({ answers: obj });
});
router.get('/candidate', (req: Request, res: Response) => {
const sessionId : string = req.header('session-id');
let connectionIds = Array.from(clients.get(sessionId));
connectionIds = connectionIds.filter(v => candidates.has(v));
const _candidates = connectionIds.map(v => { return [v, candidates.get(v)] });
const obj = _candidates.map(v => { return { "connectionId" :v[0], "candidate": v[1], }});
res.json({ candidates : obj });
});
router.put('', (req: Request, res: Response) => {
const id: string = uuid();
clients.set(id, new Set<string>());
res.json({ sessionId : id });
});
router.delete('', (req: Request, res: Response) => {
const id : string = req.header('session-id');
clients.delete(id);
res.sendStatus(200);
});
router.post('/offer', (req: Request, res: Response) => {
const sessionId : string = req.header('session-id');
let connectionIds = clients.get(sessionId);
const connectionId : string = uuid();
connectionIds.add(connectionId);
offers.set(connectionId, req.body.sdp);
res.json({ connectionId : connectionId });
});
router.post('/answer', (req: Request, res: Response) => {
const connectionId : string = req.body.connectionId;
answers.set(connectionId, req.body.sdp);
res.sendStatus(200);
});
router.post('/candidate', (req: Request, res: Response) => {
const connectionId : string = req.body.connectionId;
candidates.set(connectionId, req.body.candidate);
res.sendStatus(200);
});
export default router;

Просмотреть файл

@ -0,0 +1,7 @@
test('basic', () => {
expect('hello').toBe('hello');
});
test('basic2', () => {
expect(1+1).toBe(2);
});

Просмотреть файл

@ -0,0 +1,550 @@
{
"info": {
"_postman_id": "c4ee0e83-3ad3-400d-9947-187687d72f22",
"name": "renderstreaming",
"description": "# Introduction\nWhat does your API do?\n\n# Overview\nThings that the developers should know about\n\n# Authentication\nWhat is the preferred way of using the API?\n\n# Error Codes\nWhat errors and status codes can a user expect?\n\n# Rate limit\nIs there a limit to the number of requests an user can send?",
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
},
"item": [
{
"name": "/signaling",
"event": [
{
"listen": "test",
"script": {
"id": "38efc5aa-83d4-436a-8c8a-c80993a1dfcd",
"exec": [
"pm.test(\"Status code is 200\", function () { pm.response.to.have.status(200); });",
"pm.test(\"The response has a valid JSON body\", function () {",
" pm.response.to.be.json;",
" var jsonData = pm.response.json();",
" pm.response.to.have.jsonBody(\"sessionId\");",
"});",
"pm.environment.set(\"session_Id\", pm.response.json().sessionId);"
],
"type": "text/javascript"
}
}
],
"request": {
"method": "PUT",
"header": [],
"body": {
"mode": "raw",
"raw": ""
},
"url": {
"raw": "http://{{url}}/signaling",
"protocol": "http",
"host": [
"{{url}}"
],
"path": [
"signaling"
]
}
},
"response": []
},
{
"name": "/signaling/offer",
"event": [
{
"listen": "prerequest",
"script": {
"id": "1f7448eb-7fbe-4e06-8f57-1ab1a52f642a",
"exec": [
""
],
"type": "text/javascript"
}
},
{
"listen": "test",
"script": {
"id": "65d5877e-6458-404d-abae-3f6a2feff5d3",
"exec": [
"pm.test(\"Status code is 200\", function () { pm.response.to.have.status(200); });",
"pm.environment.set(\"connection_id\", JSON.stringify(pm.response.json().connectionId));"
],
"type": "text/javascript"
}
}
],
"request": {
"method": "POST",
"header": [
{
"key": "Content-Type",
"name": "Content-Type",
"value": "application/json",
"type": "text"
},
{
"key": "Session-Id",
"value": "{{session_Id}}",
"type": "text"
}
],
"body": {
"mode": "raw",
"raw": "{\n \"sdp\": {{sdp}}\n}"
},
"url": {
"raw": "http://{{url}}/signaling/offer",
"protocol": "http",
"host": [
"{{url}}"
],
"path": [
"signaling",
"offer"
]
}
},
"response": [
{
"name": "Default",
"originalRequest": {
"method": "POST",
"header": [],
"body": {
"mode": "raw",
"raw": ""
},
"url": {
"raw": "offer",
"host": [
"offer"
]
}
},
"code": 200,
"_postman_previewlanguage": "Text",
"header": [],
"cookie": [],
"body": ""
}
]
},
{
"name": "/signaling/answer",
"event": [
{
"listen": "prerequest",
"script": {
"id": "1f7448eb-7fbe-4e06-8f57-1ab1a52f642a",
"exec": [
""
],
"type": "text/javascript"
}
},
{
"listen": "test",
"script": {
"id": "88f991c5-074f-4040-9629-4ab3538d37d6",
"exec": [
"pm.test(\"Status code is 200\", function () { pm.response.to.have.status(200); });"
],
"type": "text/javascript"
}
}
],
"request": {
"method": "POST",
"header": [
{
"key": "Content-Type",
"name": "Content-Type",
"type": "text",
"value": "application/json"
},
{
"key": "Session-Id",
"type": "text",
"value": "{{session_Id}}"
}
],
"body": {
"mode": "raw",
"raw": "{\n\t\"connectionId\": {{connection_id}},\n\t\"sdp\" : {{sdp}}\n}"
},
"url": {
"raw": "http://{{url}}/signaling/answer",
"protocol": "http",
"host": [
"{{url}}"
],
"path": [
"signaling",
"answer"
]
}
},
"response": [
{
"name": "Default",
"originalRequest": {
"method": "POST",
"header": [],
"body": {
"mode": "raw",
"raw": ""
},
"url": {
"raw": "offer",
"host": [
"offer"
]
}
},
"code": 200,
"_postman_previewlanguage": "Text",
"header": [],
"cookie": [],
"body": ""
}
]
},
{
"name": "\b/signaling/candidate",
"event": [
{
"listen": "test",
"script": {
"id": "028da34c-ba2a-4404-a46d-82c303642cae",
"exec": [
"pm.test(\"Status code is 200\", function () { pm.response.to.have.status(200); });"
],
"type": "text/javascript"
}
}
],
"request": {
"method": "POST",
"header": [
{
"key": "Content-Type",
"name": "Content-Type",
"value": "application/json",
"type": "text"
},
{
"key": "Session-Id",
"value": "{{session_Id}}",
"type": "text"
}
],
"body": {
"mode": "raw",
"raw": "{\n\t\"connectionId\": {{connection_id}},\n\t\"candidate\" : {{candidate}}\n}"
},
"url": {
"raw": "http://{{url}}/signaling/candidate",
"protocol": "http",
"host": [
"{{url}}"
],
"path": [
"signaling",
"candidate"
]
}
},
"response": [
{
"name": "Default",
"originalRequest": {
"method": "POST",
"header": [],
"body": {
"mode": "raw",
"raw": ""
},
"url": {
"raw": "icecandidate",
"host": [
"icecandidate"
]
}
},
"code": 200,
"_postman_previewlanguage": "Text",
"header": [],
"cookie": [],
"body": ""
}
]
},
{
"name": "/",
"event": [
{
"listen": "test",
"script": {
"id": "42e4303e-4f2f-4c16-a23a-2390bf3dac32",
"exec": [
"pm.test(\"Status code is 200\", function () { pm.response.to.have.status(200); });"
],
"type": "text/javascript"
}
}
],
"request": {
"method": "GET",
"header": [],
"body": {
"mode": "raw",
"raw": ""
},
"url": {
"raw": "http://{{url}}",
"protocol": "http",
"host": [
"{{url}}"
]
}
},
"response": []
},
{
"name": "/signaling/offer",
"event": [
{
"listen": "test",
"script": {
"id": "fda4f1b3-3b8b-4c88-83bc-333ccfee434d",
"exec": [
"pm.test(\"Status code is 200\", function () { pm.response.to.have.status(200); });",
"pm.test(\"The response has a valid JSON body\", function () {",
" pm.response.to.be.json;",
" pm.response.to.have.jsonBody(\"offers\");",
" var jsonData = pm.response.json();",
" pm.expect(jsonData.offers.length).to.be.above(0);",
"});",
""
],
"type": "text/javascript"
}
}
],
"request": {
"method": "GET",
"header": [
{
"key": "Session-Id",
"value": "{{session_Id}}",
"type": "text"
}
],
"body": {
"mode": "raw",
"raw": ""
},
"url": {
"raw": "http://{{url}}/signaling/offer",
"protocol": "http",
"host": [
"{{url}}"
],
"path": [
"signaling",
"offer"
]
}
},
"response": []
},
{
"name": "/signaling/answer",
"event": [
{
"listen": "test",
"script": {
"id": "fda4f1b3-3b8b-4c88-83bc-333ccfee434d",
"exec": [
"pm.test(\"Status code is 200\", function () { pm.response.to.have.status(200); });",
"pm.test(\"The response has a valid JSON body\", function () {",
" pm.response.to.be.json;",
" pm.response.to.have.jsonBody(\"answers\");",
" var jsonData = pm.response.json();",
" pm.expect(jsonData.answers.length).to.be.above(0);",
"});",
""
],
"type": "text/javascript"
}
}
],
"request": {
"method": "GET",
"header": [
{
"key": "Session-Id",
"value": "{{session_Id}}",
"type": "text"
}
],
"body": {
"mode": "raw",
"raw": ""
},
"url": {
"raw": "http://{{url}}/signaling/answer",
"protocol": "http",
"host": [
"{{url}}"
],
"path": [
"signaling",
"answer"
]
}
},
"response": []
},
{
"name": "/signaling/candidate",
"event": [
{
"listen": "test",
"script": {
"id": "c3371877-1fc1-418d-a5f8-2829e4be4c0a",
"exec": [
"pm.test(\"Status code is 200\", function () { pm.response.to.have.status(200); });",
"pm.test(\"The response has a valid JSON body\", function () {",
" pm.response.to.be.json;",
" pm.response.to.have.jsonBody(\"candidates\");",
" var jsonData = pm.response.json();",
" pm.expect(jsonData.candidates.length).to.be.above(0);",
"});",
""
],
"type": "text/javascript"
}
}
],
"request": {
"method": "GET",
"header": [
{
"key": "Session-Id",
"value": "{{session_Id}}",
"type": "text"
}
],
"body": {
"mode": "raw",
"raw": ""
},
"url": {
"raw": "http://{{url}}/signaling/candidate",
"protocol": "http",
"host": [
"{{url}}"
],
"path": [
"signaling",
"candidate"
]
}
},
"response": []
},
{
"name": "/signaling",
"event": [
{
"listen": "test",
"script": {
"id": "38efc5aa-83d4-436a-8c8a-c80993a1dfcd",
"exec": [
"pm.test(\"Status code is 200\", function () { pm.response.to.have.status(200); });"
],
"type": "text/javascript"
}
}
],
"request": {
"method": "DELETE",
"header": [
{
"key": "Session-Id",
"value": "{{session_Id}}",
"type": "text"
}
],
"body": {
"mode": "raw",
"raw": ""
},
"url": {
"raw": "http://{{url}}/signaling",
"protocol": "http",
"host": [
"{{url}}"
],
"path": [
"signaling"
]
}
},
"response": []
}
],
"event": [
{
"listen": "prerequest",
"script": {
"id": "5b49db9f-4fc8-41c3-90b3-4d2b84c39f69",
"type": "text/javascript",
"exec": [
""
]
}
},
{
"listen": "test",
"script": {
"id": "0a8f284b-249d-40b5-be4f-e77111ed9471",
"type": "text/javascript",
"exec": [
""
]
}
}
],
"variable": [
{
"id": "eb98d1fb-ddbb-47b0-b61e-d90fed8cdbe7",
"key": "url",
"value": "localhost",
"type": "string"
},
{
"id": "e3e3a573-e02f-49c4-8348-787a0ad3fd06",
"key": "session_Id",
"value": "",
"type": "string"
},
{
"id": "0a23febf-48c8-45e2-91e6-c6d07725f36a",
"key": "sdp",
"value": "\"v=0\\r\\no=- 7060022371716902156 2 IN IP4 127.0.0.1\\r\\ns=-\\r\\nt=0 0\\r\\na=group:BUNDLE 0\\r\\na=msid-semantic: WMS 3fd630e8-9285-4f82-adbc-a7e31c334740\\r\\nm=audio 9 UDP\\/TLS\\/RTP\\/SAVPF 111 103 104 9 0 8 110 112 113 126\\r\\nc=IN IP4 0.0.0.0\\r\\na=rtcp:9 IN IP4 0.0.0.0\\r\\na=ice-ufrag:yOxE\\r\\na=ice-pwd:Ekoek1dl79NlWZS2dfmrW7Cr\\r\\na=ice-options:trickle\\r\\na=fingerprint:sha-256 02:F8:39:CE:EF:A7:77:B4:8D:56:8A:A1:C8:14:0C:1D:00:DF:99:14:ED:CE:05:4B:94:F9:EE:36:EE:4F:82:61\\r\\na=setup:actpass\\r\\na=mid:0\\r\\na=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level\\r\\na=extmap:2 http:\\/\\/www.ietf.org\\/id\\/draft-holmer-rmcat-transport-wide-cc-extensions-01\\r\\na=extmap:3 urn:ietf:params:rtp-hdrext:sdes:mid\\r\\na=extmap:4 urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id\\r\\na=extmap:5 urn:ietf:params:rtp-hdrext:sdes:repaired-rtp-stream-id\\r\\na=sendonly\\r\\na=msid:3fd630e8-9285-4f82-adbc-a7e31c334740 a573e4b8-b844-4ab9-b5b2-62c5bea85e9e\\r\\na=rtcp-mux\\r\\na=rtpmap:111 opus\\/48000\\/2\\r\\na=rtcp-fb:111 transport-cc\\r\\na=fmtp:111 minptime=10;useinbandfec=1\\r\\na=rtpmap:103 ISAC\\/16000\\r\\na=rtpmap:104 ISAC\\/32000\\r\\na=rtpmap:9 G722\\/8000\\r\\na=rtpmap:0 PCMU\\/8000\\r\\na=rtpmap:8 PCMA\\/8000\\r\\na=rtpmap:110 telephone-event\\/48000\\r\\na=rtpmap:112 telephone-event\\/32000\\r\\na=rtpmap:113 telephone-event\\/16000\\r\\na=rtpmap:126 telephone-event\\/8000\\r\\na=ssrc:2852212123 cname:GybrBKZh3U5xSqQq\\r\\na=ssrc:2852212123 msid:3fd630e8-9285-4f82-adbc-a7e31c334740 a573e4b8-b844-4ab9-b5b2-62c5bea85e9e\\r\\na=ssrc:2852212123 mslabel:3fd630e8-9285-4f82-adbc-a7e31c334740\\r\\na=ssrc:2852212123 label:a573e4b8-b844-4ab9-b5b2-62c5bea85e9e\\r\\n\"",
"type": "string"
},
{
"id": "85ad3843-bbae-41b0-b4a6-d3184d965bfe",
"key": "candidate",
"value": "\"\"",
"type": "string"
},
{
"id": "c4fd615e-6918-43ab-bdea-d5780119cb53",
"key": "connection_id",
"value": "",
"type": "string"
}
]
}

12
WebApp/tsconfig.json Normal file
Просмотреть файл

@ -0,0 +1,12 @@
{
"include": ["src/**/*"],
"exclude": ["node_modules", "**/*.spec.ts"],
"compilerOptions": {
"module": "commonjs",
"target": "es5",
"lib": ["dom","es5"],
"sourceMap": false,
"outDir":"build",
"rootDir":"src"
}
}

3
WebApp/tslint.json Normal file
Просмотреть файл

@ -0,0 +1,3 @@
{
"extends": "tslint-config-airbnb"
}

Просмотреть файл

7
pack_webapp.cmd Normal file
Просмотреть файл

@ -0,0 +1,7 @@
cd WebApp
call npm install
call npm run build
call npm run pack
cd ..\
mkdir Assets\bin~
move WebApp\*.exe Assets\bin~

Просмотреть файл

@ -1,4 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="windows-sdk-10-version-1809-all" version="10.0.17763.1" />
</packages>

6
test_webapp.cmd Normal file
Просмотреть файл

@ -0,0 +1,6 @@
cd WebApp
call npm install
call npm run lint
call npm run test
start npm run dev
call npm run newman