Browse Source

local commit

backend-api
Mustafa Yontar 5 months ago
parent
commit
eaad8a51c2
  1. 7384
      package-lock.json
  2. 3
      package.json
  3. 406
      src/lib/ObsVue.vue
  4. 105
      src/lib/PdfShare.vue
  5. 42
      src/lib/RemoteFeed.vue
  6. 112
      src/lib/RemoteFeedOnly.vue
  7. 284
      src/lib/VueJanus.vue
  8. 1110
      src/lib/webrtc_adaptor.js
  9. 13
      src/router/index.js
  10. 720
      src/views/Animator.vue
  11. 8
      src/views/Home.vue
  12. 30
      src/views/RoomView.vue
  13. 15
      src/views/StreamControl.vue
  14. 8
      src/views/login.vue
  15. 52
      src/views/remote.vue

7384
package-lock.json
File diff suppressed because it is too large
View File

3
package.json

@ -10,9 +10,12 @@
"dependencies": {
"core-js": "^3.6.4",
"jquery": "^3.4.1",
"obs-websocket-js": "^4.0.1",
"pdfjs-dist": "^2.3.200",
"vue": "^2.6.11",
"vue-native-websocket": "^2.0.14",
"vue-router": "^3.1.6",
"vue-websocket": "^0.2.3",
"vuetify": "^2.2.11",
"vuex": "^3.1.3",
"webrtc-adapter": "^7.5.1"

406
src/lib/ObsVue.vue

@ -0,0 +1,406 @@
<template>
<v-card class="fill-height align-center">
<v-dialog v-model="fileSystem">
<v-card>
<v-card-title>File System</v-card-title>
<v-card-text>
<v-file-input label="Upload" id="ufile"></v-file-input><v-btn @click="uploadfile">upload</v-btn>
<v-row>
<v-col cols="12" md="2" v-for="file in files" :key="file">
<v-card>
{{file}}
</v-card>
</v-col>
</v-row>
</v-card-text>
</v-card>
</v-dialog>
<v-dialog v-model="createSceneModel" max-width="300px">
<v-card>
<v-card-title>New Scene</v-card-title>
<v-card-text>
<v-text-field v-model="sceneName" label="Scene Name"></v-text-field>
</v-card-text>
<v-card-actions>
<v-btn text @click="createSceneModel=false">Cancel</v-btn>
<v-spacer></v-spacer>
<v-btn text color="primary" @click="createScene">Save</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
<v-dialog v-model="createSceneItem" max-width="300px">
<v-card>
<v-card-title>New Source</v-card-title>
<v-card-text>
<v-text-field v-model="sourceName" label="Source Name"></v-text-field>
<v-select v-model="sourceType" item-value="value" item-text="name" :items="[{name:'Image', value:'image_source'},{name: 'VLC Source', value: 'vlc_source'},{name: 'Media Source', value: 'ffmpeg_source'},{name: 'Text', value: 'text_ft2_source_v2'}]" label="Source Type"></v-select>
<v-text-field label="Image Path" v-model="sourceSettings.file" v-if="sourceType === 'image_source'"></v-text-field>
<v-text-field label="File Path" v-model="sourceSettings.localFile" v-if="sourceType === 'ffmpeg_source'"></v-text-field>
<div v-if="sourceType === 'vlc_source'">
<div v-for="(file, index) in sourceSettings.playlist" :key="index">
{{file}}
</div>
</div>
<v-text-field v-if="sourceType === 'vlc_source'" v-model="sourceSettings.file"></v-text-field><v-btn small text @click="addPlaylist(sourceSettings.file)">+</v-btn>
</v-card-text>
<v-card-actions>
<v-btn text @click="createSceneItem=false">Cancel</v-btn>
<v-spacer></v-spacer>
<v-btn text color="primary" @click="createSource">Save</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
<v-card-text>
<v-row>
<v-col cols="12" md="9">
<v-card ref="mainvid" id="mainvid"
:style="`font-size:10px;height: ${vidheight}px;background: #ccc;position: relative`"
v-if="selecteScene">
<div :style="`border:1px solid #ccc;width:${source.cx*vidperm}px;z-index:${selecteScene.sources.length - index};height:${source.cy*vidperm}px;background-size: 100% 100%;top:${source.y*vidperm}px;left:${source.x*vidperm}px;position:absolute;min-width:50px;min-height:50px;background:url()`"
v-for="(source,index) in selecteScene.sources" :key="index">
<div style="top:0;left:0;position: absolute;color:#fff;text-shadow: #000 1px 1px">
{{source.name}} ({{source.id}})
</div>
<div style="top:0;right:0;position: absolute;color:#fff;text-shadow: #000 1px 1px">
{{source.type}} ({{source.source_cx}} x {{source.source_cy}} )
</div>
<img v-if="source.srcdata" width="100%" height="100%" :src="source.srcdata"/>
</div>
</v-card>
</v-col>
<v-col cols="12" md="3">
<v-card v-if="selectedItemProps">
<v-card-text>
<v-text-field label="Name" v-model="selectedItemProps.name"></v-text-field>
<v-text-field :label="`Width (Source: ${selectedItemProps.sourceWidth})`"
v-model="selectedItemProps.width" @keyup="selectedItemProps.height=selectedItemProps.width * (selectedItemProps.sourceHeight/selectedItemProps.sourceWidth)"></v-text-field>
<v-text-field :label="`Height (Source: ${selectedItemProps.sourceHeight})`"
v-model="selectedItemProps.height"></v-text-field>
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn small @click="saveSelectedItem">Save</v-btn>
</v-card-actions>
</v-card>
<br/>
<v-card v-if="selectedItemSourceProps">
<v-card-title>Source Options
<v-spacer></v-spacer>
<v-btn icon x-smal @click="panels.source = !panels.source">
<v-icon>mdi-menu</v-icon>
</v-btn>
</v-card-title>
<v-card-text v-show="panels.source">
<v-text-field v-if="selectedItemSourceProps.sourceType === 'xcomposite_input'"
label="Capture Window"
v-model="selectedItemSourceProps.sourceSettings.capture_window"></v-text-field>
<v-text-field v-if="selectedItemSourceProps.sourceType === 'image_source'" label="File"
v-model="selectedItemSourceProps.sourceSettings.file"></v-text-field>
<div v-if="selectedItemSourceProps.sourceType === 'vlc_source'">
<div v-for="(item, index) in selectedItemSourceProps.sourceSettings.playlist"
:key="index">
<v-row>
<v-col cols="12" md="10">{{item.value}}</v-col>
<v-col cols="12" md="2">
<v-btn icon small
@click="selectedItemSourceProps.sourceSettings.playlist.splice(index,1)">
<v-icon>mdi-delete</v-icon>
</v-btn>
</v-col>
</v-row>
</div>
<v-text-field v-if="selectedItemSourceProps.sourceType === 'vlc_source'" label="File"
v-model="selectedItemSourceProps.file"></v-text-field>
<v-spacer></v-spacer>
<v-btn small
@click="selectedItemSourceProps.sourceSettings.playlist.push({ hidden: false, selected: false, value: selectedItemSourceProps.file}); selectedItemSourceProps.file=''">
Add To Playlist
</v-btn>
</div>
</v-card-text>
<v-card-actions v-show="panels.source">
<v-spacer></v-spacer>
<v-btn small>Save</v-btn>
</v-card-actions>
</v-card>
<br/>
<v-card v-if="selectedItemProps">
<v-card-title>Position
<v-spacer></v-spacer>
<v-btn icon x-smal @click="panels.position = !panels.position" l>
<v-icon>mdi-menu</v-icon>
</v-btn>
</v-card-title>
<v-card-text v-show="panels.position">
<v-text-field label="X" v-model="selectedItemProps.position.x"></v-text-field>
<v-text-field label="Y" v-model="selectedItemProps.position.y"></v-text-field>
<v-text-field label="Rotation" v-model="selectedItemProps.rotation"></v-text-field>
</v-card-text>
<v-card-actions v-show="panels.position">
<v-spacer></v-spacer>
<v-btn small>Save</v-btn>
</v-card-actions>
</v-card>
<br/>
<v-card v-if="selectedItemProps">
<v-card-title>Crop
<v-spacer></v-spacer>
<v-btn icon x-smal @click="panels.crop = !panels.crop">
<v-icon>mdi-menu</v-icon>
</v-btn>
</v-card-title>
<v-card-text v-show="panels.crop">
<v-text-field label="Top" v-model="selectedItemProps.crop.top"></v-text-field>
<v-text-field label="Left" v-model="selectedItemProps.crop.left"></v-text-field>
<v-text-field label="Bottom" v-model="selectedItemProps.crop.bottom"></v-text-field>
<v-text-field label="Right" v-model="selectedItemProps.crop.right"></v-text-field>
</v-card-text>
<v-card-actions v-show="panels.crop">
<v-spacer></v-spacer>
<v-btn small>Save</v-btn>
</v-card-actions>
</v-card>
</v-col>
</v-row>
<v-row>
<v-col cols="12" md="3">
<v-list>
<v-list-item-group v-model="selecteSceneName" color="primary">
<v-list-item v-for="(scene,index) in scenes" :key="index" @click="setSelect(scene)">
<v-list-item-content>
<v-list-item-title v-text="scene.name"></v-list-item-title>
</v-list-item-content>
</v-list-item>
<v-list-item>
<v-list-item-content>
<v-list-item-title @click="createSceneModel=true">New Scene</v-list-item-title>
</v-list-item-content>
</v-list-item>
</v-list-item-group>
</v-list>
</v-col>
<v-col cols="12" md="3">
<v-list v-if="selecteScene">
<v-list-item v-for="(source,index) in selecteScene.sources" :key="index"
@click="setItemSelect(source)">
<v-list-item-content>
<v-list-item-title v-text="source.name"></v-list-item-title>
</v-list-item-content>
</v-list-item>
<v-list-item>
<v-list-item-content>
<v-list-item-title @click="createSceneItem=true">New Item</v-list-item-title>
</v-list-item-content>
</v-list-item>
</v-list>
</v-col>
</v-row>
</v-card-text>
</v-card>
</template>
<script>
const OBSWebSocket = require('obs-websocket-js');
export default {
name: "ObsVue",
props: {
server: String
},
data: () => ({
obs: null,
selectedItemProps: null,
selectedItemSourceProps: null,
createSceneModel: false,
createSceneItem: false,
sourceSettings: {
playlist: [],
file: '',
localFile: ''
},
files: [
'test.jpg',
'test.png'
],
sourceType: '',
sourceName: '',
scenes: [],
sources: [],
panels: {
source: false,
position: false,
crop: false
},
previewInterval: 0,
selecteScene: null,
sceneName: '',
vidheight: 0,
vidperm: 0,
selecteSceneName: null,
fileSystem: true
}),
mounted() {
this.obs = new OBSWebSocket()
this.getfiles()
this.obs.connect({address: 'ws://localhost:4444', password: 'neverdie'}).then(this.connected)
this.obs.on('error', err => {
console.error('socket error:', err);
});
},
methods: {
uploadfile() {
var file = document.getElementById('ufile')
console.log(file.files[0])
var filereader = new FileReader()
filereader.onloadend = () => {
this.$axios.post('http://localhost:5000/api/upload', {"file": filereader.result.split(';base64,')[1], 'file_name': file.files[0].name}).then(this.getfiles)
}
filereader.readAsDataURL(file.files[0]);
},
getfiles () {
this.$axios.get('http://localhost:5000/api/files').then(result => {
this.files = result.data
})
},
addPlaylist(item) {
this.sourceSettings.playlist.push({hidden: false, selected: false, value:item})
console.log(this.sourceSettings)
},
createScene() {
this.obs.send('CreateNewScene', {'sceneName': this.sceneName}).then(response => {
console.log(response)
this.sceneName = ''
this.createSceneModel = false
this.sceneList()
})
},
createSource() {
var sourceSetting;
if (this.sourceType === 'vlc_source') sourceSetting = {playlist: this.sourceSettings.playlist}
if (this.sourceType === 'ffmpeg_source') sourceSetting = {localfile: this.sourceSettings.localfile}
if (this.sourceType === 'image_source') sourceSetting = {file: this.sourceSettings.file}
console.log({sourceName: this.sourceName,sourceSettings: sourceSetting, sourceType: this.sourceType })
this.obs.send('CreateNewSource', {sourceName: this.sourceName,sourceSettings: sourceSetting, sourceType: this.sourceType }).then(response => {
console.log(response)
this.sceneName = ''
this.createSceneModel = false
this.obs.send('AddToScene', {sourceName:this.sourceName })
this.sceneList()
})
},
onAuth() {
console.log("auth")
},
newScene() {
},
setItemSelect(item) {
this.obs.send("GetSceneItemProperties", {
item: item.name,
"scene-name": this.selecteScene.name
}).then(data => {
console.log("GetSceneItemProperties")
console.log(data)
this.selectedItemProps = data
})
this.obs.send("GetSourceSettings", {sourceName: item.name}).then(data => {
console.log("GetSourceSettings")
console.log(data)
this.selectedItemSourceProps = data
})
},
setSelect(item) {
console.log(item)
this.selecteScene = item
this.selecteSceneName = item.name
this.obs.send('GetSourcesList').then(data => {
console.log('GetSourcesList')
console.log(data)
})
if (this.previewInterval > 0) clearInterval(this.previewInterval)
this.previewInterval = setInterval(() => {
this.selecteScene.sources.forEach(sitem => {
this.obs.send('TakeSourceScreenshot', {
sourceName: sitem.name,
embedPictureFormat: 'png'
}).then(data => {
sitem.srcdata = data.img
this.selecteScene.__ob__.dep.notify()
this.vidheight = document.getElementById('mainvid').offsetWidth / (16 / 9)
this.vidperm = this.vidheight / 1080.0
}).catch(err => {
console.log(err)
})
})
}, 500)
},
saveSelectedItem() {
console.log( {
'item': this.selectedItemProps.name,
position: {
x: parseInt(this.selectedItemProps.position.x),
y: parseInt(this.selectedItemProps.position.y),
},
crop: this.selectedItemProps.crop,
rotation: this.selectedItemProps.rotation,
height: this.selectedItemProps.height,
scale: {x:this.selectedItemProps.width/this.selectedItemProps.sourceWidth,y:this.selectedItemProps.height/this.selectedItemProps.sourceHeight}
})
this.obs.send('SetSceneItemProperties', {
item: this.selectedItemProps.name,
position: {
x: parseInt(this.selectedItemProps.position.x),
y: parseInt(this.selectedItemProps.position.y),
},
crop: {
top:parseInt(this.selectedItemProps.crop.top),
bottom:parseInt(this.selectedItemProps.crop.bottom),
left:parseInt(this.selectedItemProps.crop.left),
right:parseInt(this.selectedItemProps.crop.right),
},
rotation: this.selectedItemProps.rotation,
height: this.selectedItemProps.height,
scale: {x:this.selectedItemProps.width/this.selectedItemProps.sourceWidth,y:this.selectedItemProps.height/this.selectedItemProps.sourceHeight}
}).then(data => {
console.log(data)
this.sceneList()
})
},
sceneList() {
this.obs.send('GetSceneList').then(data => {
console.log("scene", data)
this.scenes = data.scenes
data.scenes.forEach(item => {
if (item.name === data["current-scene"]) {
this.setSelect(item)
}
})
})
},
connected() {
this.obs.send('GetVersion').then(data => {
this.obs.send('GetVersion').then(data2 => {
console.log(data)
console.log(data2)
})
})
this.sceneList()
}
}
}
</script>
<style scoped>
</style>

105
src/lib/PdfShare.vue

@ -25,6 +25,7 @@
<script>
import pdfjsLib from 'pdfjs-dist/webpack';
import WebRTCAdaptor from "@/lib/webrtc_adaptor";
const toBase64 = file => new Promise((resolve, reject) => {
const reader = new FileReader();
reader.readAsDataURL(file);
@ -39,6 +40,7 @@ export default {
onstop: Function,
username: String,
myrivid: Number,
streamer:Boolean,
room: Number,
opaqueId:String
},
@ -52,6 +54,100 @@ export default {
pdfjsLib.GlobalWorkerOptions.workerSrc = "/pdf.worker.js"
},
methods: {
streamerScreenShare(stream) {
let websocketURL = "wss://live.ozgurkon.org/LiveApp/websocket"
var mediaConstraints = {
video: { width: 1280, height: 720 },
audio: false
};
var pc_config = null;
var sdpConstraints = {
OfferToReceiveAudio: false,
OfferToReceiveVideo: false
};
if (this.streamer) {
var tt = this
this.antwrtcScreen = new WebRTCAdaptor({
websocket_url: websocketURL,
mediaConstraints: mediaConstraints,
peerconnection_config: pc_config,
sdp_constraints: sdpConstraints,
janusScreen:stream,
localVideoId: "myvideo2",
debug: true,
callback: function (info, obj) {
if (info == "initialized") {
console.log("initialized");
tt.antwrtcScreen.publish(tt.username+"-pdf", "null");
// start_publish_button.disabled = false;
// stop_publish_button.disabled = true;
} else if (info == "publish_started") {
//stream is being published
console.log("publish started");
// start_publish_button.disabled = true;
// stop_publish_button.disabled = false;
// startAnimation();
} else if (info == "publish_finished") {
//stream is being finished
console.log("publish finished");
this.startPublishing(tt.username + "-pdf");
// start_publish_button.disabled = false;
// stop_publish_button.disabled = true;
} else if (info == "screen_share_extension_available") {
// screen_share_checkbox.disabled = false;
// console.log("screen share extension available");
// install_extension_link.style.display = "none";
} else if (info == "screen_share_stopped") {
console.log("screen share stopped");
} else if (info == "closed") {
//console.log("Connection closed");
if (typeof obj != "undefined") {
console.log("Connecton closed: " + JSON.stringify(obj));
}
} else if (info == "pong") {
//ping/pong message are sent to and received from server to make the connection alive all the time
//It's especially useful when load balancer or firewalls close the websocket connection due to inactivity
} else if (info == "refreshConnection") {
this.startPublishing(tt.username + "-pdf");
} else if (info == "updated_stats") {
//obj is the PeerStats which has fields
//averageOutgoingBitrate - kbits/sec
//currentOutgoingBitrate - kbits/sec
console.log("Average outgoing bitrate " + obj.averageOutgoingBitrate + " kbits/sec"
+ " Current outgoing bitrate: " + obj.currentOutgoingBitrate + " kbits/sec");
}
},
callbackError: function (error, message) {
//some of the possible errors, NotFoundError, SecurityError,PermissionDeniedError
console.log("error callback: " + JSON.stringify(error));
var errorMessage = JSON.stringify(error);
if (typeof message != "undefined") {
errorMessage = message;
}
if (error.indexOf("NotFoundError") != -1) {
errorMessage = "Camera or Mic are not found or not allowed in your device";
} else if (error.indexOf("NotReadableError") != -1 || error.indexOf("TrackStartError") != -1) {
errorMessage = "Camera or Mic is being used by some other process that does not let read the devices";
} else if (error.indexOf("OverconstrainedError") != -1 || error.indexOf("ConstraintNotSatisfiedError") != -1) {
errorMessage = "There is no device found that fits your video and audio constraints. You may change video and audio constraints"
} else if (error.indexOf("NotAllowedError") != -1 || error.indexOf("PermissionDeniedError") != -1) {
errorMessage = "You are not allowed to access camera and mic.";
} else if (error.indexOf("TypeError") != -1) {
errorMessage = "Video/Audio is required";
}
alert(errorMessage);
}
});
}
},
renderPage(pageNumber) {
this.pdf.getPage(pageNumber).then(page => {
console.log('Page loaded');
@ -126,7 +222,7 @@ export default {
console.log("Janus " + (on ? "started" : "stopped") + " receiving our " + medium);
},
webrtcState (on) {
this.pluginHandle.send({"message": { "request": "configure", "bitrate": 1024*1000 }});
this.pluginHandle.send({"message": { "request": "configure", "bitrate": 512*1000 }});
console.log(on, "webrt")
},
janusMessage (msg, jsep) {
@ -191,8 +287,13 @@ export default {
onlocalstream (stream) {
// Janus.attachMediaStream($('#myvideo').get(0), stream);
console.log('pdf --------- onlocalstream')
console.log('pdf --------- onlocalstream')
console.log('pdf --------- onlocalstream')
console.log('pdf --------- onlocalstream')
console.log('pdf --------- onlocalstream')
this.streamerScreenShare(stream)
console.log(stream)
},
async loadFile() {

42
src/lib/RemoteFeed.vue

@ -1,12 +1,15 @@
<template>
<v-card max-height="150" height="150" dark
:color="remoteStream.talking?'green':''"
<div>
<v-card dark v-if="!singleView"
:color="remoteStream.talking && !singleView?'green':''"
class="vid_main" @mouseleave="showaction = false" @mouseover="showaction = true">
<video ref="feed_video" class="vid_container" autoplay playsinline/>
<div class="infor">{{ bitrate }}</div>
<div class="infoSlow">Slow Connection</div>
<div class="infoName">{{ remoteStream.display }}</div>
<div class="actions" v-show="showaction">
<video ref="feed_video" class="vid_container" width="99%" height="99%" autoplay playsinline/>
<div v-if="!singleView" class="infor">{{ bitrate }}</div>
<div class="kick" v-if="creator && !singleView"><v-btn icon @click="$emit('kick',feedid)"><v-icon>mdi-delete</v-icon></v-btn></div>
<div class="rtp" v-if="creator && !singleView"><v-btn icon @click="$emit('rtp',feedid)"><v-icon>mdi-pencil</v-icon></v-btn></div>
<div v-if="!singleView" class="infoSlow">Slow Connection</div>
<div v-if="!singleView" class="infoName">{{ remoteStream.display }}</div>
<div v-if="!singleView" class="actions" v-show="showaction">
<v-slider
v-model="volume"
:min="0"
@ -15,6 +18,9 @@
></v-slider>
</div>
</v-card>
<video ref="feed_video" width="1280" height="720" v-else class="vid_container" autoplay playsinline/>
</div>
</template>
<script>
@ -23,7 +29,9 @@
janusInit: Object,
opaqueId: String,
room: Number,
singleView: Boolean,
mypvtid: Number,
creator: Boolean,
feedid: Number,
remoteStream: Object
},
@ -37,6 +45,9 @@
volume: 100
}),
watch: {
feedid () {
document.location.reload()
},
volume (old, newVal) {
this.$refs.feed_video.volume = parseFloat(newVal) * 0.01
@ -72,6 +83,7 @@
this.remoteFeed.simulcastStarted = false;
console.log('Remote feed success')
var subscribe = { "request": "join", "room": this.room, "ptype": "subscriber", "feed": this.feedid, "private_id": this.mypvtid };
console.log(subscribe)
this.remoteFeed.send({"message": subscribe});
@ -81,7 +93,9 @@
},
onStream (stream) {
console.log('stream is strarted')
this.$emit('stream',stream)
if (this.$refs.feed_video) {
try {
this.$refs.feed_video.srcObject = stream
} catch (e) {
@ -175,6 +189,20 @@
font-size: 10px;
text-shadow: 1px 1px #000;
}
.kick {
position: absolute;
right: 0;
top:30px;
font-size: 10px;
text-shadow: 1px 1px #000;
}
.rtp {
position: absolute;
left: 0;
top:30px;
font-size: 10px;
text-shadow: 1px 1px #000;
}
.infoName {
position: absolute;
left: 5px;

112
src/lib/RemoteFeedOnly.vue

@ -0,0 +1,112 @@
<template>
<RemoteFeed v-if="remoteStream" :single-view="true" :opaqueId="opaqueId" :mypvtid="mypvtid" :feedid="remoteStream.id" :remote-stream="remoteStream" :janusInit="janusInit" :room="1234" ></RemoteFeed>
</template>
<script>
import RemoteFeed from "@/lib/RemoteFeed";
export default {
name: "RemoteFeedOnly",
props: {
server: String,
room: Number,
user: String
},
components: {RemoteFeed},
data: () => ({
remoteStream: null,
opaqueId: null,
mypvtid: null,
janusInit: null,
run: false
}),
mounted() {
this.janusInit = new this.$janus({
success: this.janusSuccess,
server: this.server,
error: function (error) {
this.$janus.error(error);
},
//webrtcState: this.webrtcState,
destroyed: function () {
window.location.reload();
}
})
},
methods: {
janusMessage(msg, jsep) {
console.log(msg);
var event = msg["videoroom"];
if (event !== undefined) {
if (event === "joined") {
this.myid = msg["id"];
this.mypvtid = msg["private_id"];
this.$janus.log("Successfully joined room " + msg["room"] + " with ID " + this.myid);
// this.restartDevices()
if (msg["publishers"] !== undefined && msg["publishers"] !== null) {
msg['publishers'].forEach(publisher => {
console.log(publisher.id)
console.log(publisher)
if (publisher.display === this.user) {
this.remoteStream = publisher
this.run = true
}
});
}
} else if (event === "event") {
if (msg["publishers"] !== undefined && msg["publishers"] !== null) {
msg['publishers'].forEach(publisher => {
console.log(publisher.id)
console.log(publisher)
if (publisher.display === this.user) {
this.remoteStream = publisher
this.run = true
}
});
} else if (msg["leaving"] !== undefined && msg["leaving"] !== null) {
console.log("leaving")
}
}
}
if (jsep !== undefined && jsep !== null) {
console.log("Handling SDP as well...");
console.log(jsep);
this.pluginHandle.handleRemoteJsep({jsep: jsep});
var audio = msg["audio_codec"];
if (this.mystream && this.mystream.getAudioTracks() && this.mystream.getAudioTracks().length > 0 && !audio) {
console.log("Our audio stream has been rejected, viewers won't hear us");
}
}
},
janusPluginSuccess (pluginHandle) {
this.pluginHandle = pluginHandle
this.$janus.log("Plugin attached! (" + this.pluginHandle.getPlugin() + ", id=" + this.pluginHandle.getId() + ")");
this.$janus.log(" -- This is a publisher/manager");
var register = { "request": "join", "room": this.room, "ptype": "publisher" };
pluginHandle.send({"message": register});
},
janusSuccess() {
this.opaqueId = "videoroomtest-" + this.$janus.randomString(12);
this.janusInit.attach(
{
plugin: "janus.plugin.videoroom",
opaqueId: this.opaqueId,
success: this.janusPluginSuccess,
error: this.janusPluginError,
onmessage: this.janusMessage,
}
)
}
}
}
</script>
<style scoped>
</style>

284
src/lib/VueJanus.vue

@ -1,6 +1,6 @@
<template>
<v-container fill-height>
<PdfShare v-if="sharePdf" :username="username" :opaque-id="opaqueId" :room="room" :janus-init="janusInit" ></PdfShare>
<PdfShare v-if="sharePdf" :streamer="streamer" :username="username" :opaque-id="opaqueId" :room="1234" :janus-init="janusInit" ></PdfShare>
<v-dialog
v-model="mozillaAlert"
max-width="600"
@ -85,22 +85,37 @@
</v-card-actions>
</v-card>
</v-dialog>
<div v-show="remoteViewNew">
<div style="width: 100%;height: 100%;background: #ccc;position: absolute;top:0;left:0"></div>
<v-row>
<v-col cols="12" md="10"></v-col>
<v-col cols="12" md="2">
<v-row>
<v-col cols="12" md="12" v-for="remoteStream in remoteStreams" :key="remoteStream.id">
<RemoteFeed v-if="remoteViewNew" @kick="kickUser" @rtp="$emit('rtp',remoteStream.id)" :creator="creator" :talking="remoteStream.talking" :opaqueId="opaqueId" :mypvtid="mypvtid" :feedid="remoteStream.id" :remote-stream="remoteStream" :janusInit="janusInit" :room="1234" ></RemoteFeed>
</v-col>
</v-row>
</v-col>
</v-row>
<v-row>
<v-col cols="12" md="2">
</div>
<v-row v-show="!remoteViewNew">
<v-col cols="12" :md="mdsize">
<div width="100%" height="100%" style="position: relative">
<video ref="ownstream" class="rounded centered" id="myvideo" width="100%" height="100%" autoplay
<video ref="ownstream" v-if="streamer" class="rounded centered" id="myvideo" width="100%" height="100%" autoplay
playsinline muted="muted"/>
<video ref="ownstream" v-if="!streamer" class="rounded centered" id="myvideo" width="100%" height="100%" autoplay
playsinline muted="muted"/>
<v-btn icon v-if="ownmuted" @click="muteMe"><v-icon>mdi-volume-mute</v-icon></v-btn>
<v-btn icon v-if="!ownmuted" @click="muteMe"><v-icon>mdi-volume-high</v-icon></v-btn>
</div>
</v-col>
<v-col cols="12" md="2" style="display: none">
<v-col cols="12" :md="mdsize">
<video ref="ownstreamscreen" class="rounded centered" id="myvideo2" width="100%" height="100%" autoplay
playsinline muted="muted"/>
</v-col>
<v-col cols="12" md="2" v-for="remoteStream in remoteStreams" :key="remoteStream.id">
<RemoteFeed @kick="kickUser" :creator="creator" :talking="remoteStream.talking" :opaqueId="opaqueId" :mypvtid="mypvtid" :feedid="remoteStream.id" :remote-stream="remoteStream" :janusInit="janusInit" :room="room" ></RemoteFeed>
<v-col cols="12" :md="mdsize" v-for="remoteStream in remoteStreams" :key="remoteStream.id">
<RemoteFeed v-if="!remoteViewNew" @kick="kickUser" @rtp="$emit('rtp',remoteStream.id)" :creator="creator" :talking="remoteStream.talking" :opaqueId="opaqueId" :mypvtid="mypvtid" :feedid="remoteStream.id" :remote-stream="remoteStream" :janusInit="janusInit" :room="1234" ></RemoteFeed>
</v-col>
</v-row>
<v-speed-dial
@ -154,6 +169,21 @@
<span>Toggle Theme</span>
</v-tooltip>
<v-tooltip right>
<template v-slot:activator="{ on }">
<v-btn
v-on="on"
fab
dark
small
@click="remoteViewNew = !remoteViewNew"
>
<v-icon>mdi-compare</v-icon>
</v-btn>
</template>
<span>Toggle View</span>
</v-tooltip>
<v-tooltip right>
<template v-slot:activator="{ on }">
@ -194,21 +224,26 @@
<script>
import RemoteFeed from "@/lib/RemoteFeed";
import PdfShare from "@/lib/PdfShare";
import WebRTCAdaptor from "@/lib/webrtc_adaptor";
export default {
components: {PdfShare, RemoteFeed},
props: {
server: String,
room: Number,
stream: Boolean,
username: String,
creator: Boolean
creator: Boolean,
streamer: Boolean
},
data() {
return {
janusInit: null,
antwrtc: null,
opaqueId: null,
pluginHandle: null,
mystream: null,
myid: '',
mdsize:6,
ownmuted: false,
mypvtid: '',
selectedVideo: null,
@ -216,6 +251,7 @@
started: false,
mozillaAlert: false,
sharePdf: false,
remoteViewNew: false,
janusScreenShareHandle: null,
chromeAlert: false,
videoDevices: [],
@ -226,7 +262,8 @@
fab: true,
shareScreenId: null,
remoteStreams: [],
screenShareStarted: false
screenShareStarted: false,
antwrtcScreen: false
}
},
name: "VueJanus",
@ -265,24 +302,130 @@
this.pluginHandle.send({"message": { "request": "configure", "bitrate": 128*1000 }});
console.log(on, "webrt")
},
webrtcState2 (on) {
this.pluginHandle.send({"message": { "request": "configure", "bitrate": 128*1000 }});
console.log(on, "webrt")
//this.antwrtc.publish(this.username, "null");
},
janusPluginSuccessScreen (pluginHandle) {
this.janusScreenShareHandle = pluginHandle
this.$janus.log("Plugin attached! (" + this.pluginHandle.getPlugin() + ", id=" + this.pluginHandle.getId() + ")");
this.$janus.log(" -- This is a publisher/manager");
var register = { "request": "join", "room": this.room, "ptype": "publisher", "display": this.username + " - Share Screen" };
var register = { "request": "join", "room": 1234, "ptype": "publisher", "display": this.username + " - Share Screen" };
pluginHandle.send({"message": register});
},
castStart(jsep) {
var publish = { "request": "configure", "audio": false, "video": true };
this.janusScreenShareHandle.send({"message": publish, "jsep": jsep});
},
streamerScreenShare(stream) {
let websocketURL = "wss://live.ozgurkon.org/LiveApp/websocket"
var mediaConstraints = {
video: { width: 1280, height: 720 },
audio: false
};
var pc_config = null;
var sdpConstraints = {
OfferToReceiveAudio: false,
OfferToReceiveVideo: false
};
if (this.streamer) {
var tt = this
this.antwrtcScreen = new WebRTCAdaptor({
websocket_url: websocketURL,
mediaConstraints: mediaConstraints,
peerconnection_config: pc_config,
sdp_constraints: sdpConstraints,
janusScreen:stream,
localVideoId: "myvideo2",
debug: true,
callback: function (info, obj) {
if (info == "initialized") {
console.log("initialized");
tt.antwrtcScreen.publish(tt.username+"-desktop", "null");
// start_publish_button.disabled = false;
// stop_publish_button.disabled = true;
} else if (info === "publish_started") {
//stream is being published
console.log("publish started");
// start_publish_button.disabled = true;
// stop_publish_button.disabled = false;
// startAnimation();
} else if (info == "publish_finished") {
//stream is being finished
console.log("publish finished");
// start_publish_button.disabled = false;
// stop_publish_button.disabled = true;
} else if (info == "screen_share_extension_available") {
// screen_share_checkbox.disabled = false;
// console.log("screen share extension available");
// install_extension_link.style.display = "none";
} else if (info == "screen_share_stopped") {
console.log("screen share stopped");
} else if (info == "closed") {
//console.log("Connection closed");
if (typeof obj != "undefined") {
console.log("Connecton closed: " + JSON.stringify(obj));
}
} else if (info == "pong") {
//ping/pong message are sent to and received from server to make the connection alive all the time
//It's especially useful when load balancer or firewalls close the websocket connection due to inactivity
} else if (info == "refreshConnection") {
this.startPublishing();
} else if (info == "updated_stats") {
//obj is the PeerStats which has fields
//averageOutgoingBitrate - kbits/sec
//currentOutgoingBitrate - kbits/sec
console.log("Average outgoing bitrate " + obj.averageOutgoingBitrate + " kbits/sec"
+ " Current outgoing bitrate: " + obj.currentOutgoingBitrate + " kbits/sec");
}
},
callbackError: function (error, message) {
//some of the possible errors, NotFoundError, SecurityError,PermissionDeniedError
console.log("error callback: " + JSON.stringify(error));
var errorMessage = JSON.stringify(error);
if (typeof message != "undefined") {
errorMessage = message;
}
if (error.indexOf("NotFoundError") != -1) {
errorMessage = "Camera or Mic are not found or not allowed in your device";
} else if (error.indexOf("NotReadableError") != -1 || error.indexOf("TrackStartError") != -1) {
errorMessage = "Camera or Mic is being used by some other process that does not let read the devices";
} else if (error.indexOf("OverconstrainedError") != -1 || error.indexOf("ConstraintNotSatisfiedError") != -1) {
errorMessage = "There is no device found that fits your video and audio constraints. You may change video and audio constraints"
} else if (error.indexOf("NotAllowedError") != -1 || error.indexOf("PermissionDeniedError") != -1) {
errorMessage = "You are not allowed to access camera and mic.";
} else if (error.indexOf("TypeError") != -1) {
errorMessage = "Video/Audio is required";
}
alert(errorMessage);
}
});
}
},
janusMessageScreen (msg, jesp) {
var event = msg["videoroom"]
console.log(jesp)
if(event === "joined") {
this.janusScreenShareHandle.createOffer(
{
media: { video: this.capture, audioSend: false, videoRecv: false}, // Screen sharing Publishers are sendonly
simulcast:true,
simulcast2:true,
success: this.castStart,
error: function(error) {
console.log("WebRTC error:", error);
@ -299,7 +442,7 @@
onlocalstreamScreen (stream) {
if(this.screenShareStarted)
this.screenShareStarted=true;
this.streamerScreenShare(stream)
this.$refs.ownstreamscreen.srcObject = stream
},
screenShareSwitch () {
@ -319,7 +462,7 @@
consentDialog: this.consentDialog,
error: this.janusPluginError,
mediaState: this.mediaState,
webrtcState: this.webrtcState,
webrtcState: this.webrtcState2,
onmessage: this.janusMessageScreen,
onlocalstream: this.onlocalstreamScreen
}
@ -393,6 +536,19 @@
this.pluginHandle.send({"message": publish, "jsep": jsep});
},
controlPublishers(msg) {
if (msg["publishers"].length > 12) {
this.mdsize = 1
} else {
var size = parseInt(18 / (msg["publishers"].length+1))
if(size<6) {
this.mdsize = size
} else {
this.mdsize = size /2
}
if (this.mdsize > 6) this.mdsize = 6
this.mdsize = parseInt(this.mdsize)
}
msg["publishers"].forEach(item => {
var has = false
item.talking = false
@ -464,18 +620,24 @@
var media = {
audioRecv: false, videoRecv: false, audioSend: true, videoSend: !this.noVid,
simulcast:true,
simulcast2:true,
audio: {
deviceId: {
exact: this.selectedAudio
}
},
replaceAudio: true, // This is only needed in case of a renegotiation
// replaceAudio: true, // This is only needed in case of a renegotiation
video: {
deviceId: {
exact: this.selectedVideo
}
exact: this.selectedVideo,
width:1280,
height: 720
},
width:1280,
height: 720
},
replaceVideo: true,
// replaceVideo: true,
};
@ -496,18 +658,104 @@
this.$refs.ownstream.setSinkId(this.selectedVideo)
this.$refs.ownstream.setSinkId(this.selectedAudio)
},
selectDevice () {
selectDevice: function () {
if (!this.started) {
console.log(this.selectedVideo)
console.log(this.selectedAudio)
this.started = true
var register = {
"request": "join",
"room": this.room,
"room": 1234,
"ptype": "publisher",
"display": this.username
};
this.pluginHandle.send({"message": register});
let websocketURL = "wss://live.ozgurkon.org/LiveApp/websocket"
var mediaConstraints = {
video: { width: 1280, height: 720 },
audio: true
};
var pc_config = null;
var sdpConstraints = {
OfferToReceiveAudio: false,
OfferToReceiveVideo: false
};
if (this.streamer) {
this.antwrtc = new WebRTCAdaptor({
websocket_url: websocketURL,
mediaConstraints: mediaConstraints,
peerconnection_config: pc_config,
sdp_constraints: sdpConstraints,
localVideoId: "myvideo",
debug: true,
callback: function (info, obj) {
if (info == "initialized") {
console.log("initialized");
// start_publish_button.disabled = false;
// stop_publish_button.disabled = true;
} else if (info == "publish_started") {
//stream is being published
console.log("publish started");
// start_publish_button.disabled = true;
// stop_publish_button.disabled = false;
// startAnimation();
} else if (info == "publish_finished") {
//stream is being finished
console.log("publish finished");
// start_publish_button.disabled = false;
// stop_publish_button.disabled = true;
} else if (info == "screen_share_extension_available") {
// screen_share_checkbox.disabled = false;
// console.log("screen share extension available");
// install_extension_link.style.display = "none";
} else if (info == "screen_share_stopped") {
console.log("screen share stopped");
} else if (info == "closed") {
//console.log("Connection closed");
if (typeof obj != "undefined") {
console.log("Connecton closed: " + JSON.stringify(obj));
}
} else if (info == "pong") {
//ping/pong message are sent to and received from server to make the connection alive all the time
//It's especially useful when load balancer or firewalls close the websocket connection due to inactivity
} else if (info == "refreshConnection") {
this.startPublishing();
} else if (info == "updated_stats") {
//obj is the PeerStats which has fields
//averageOutgoingBitrate - kbits/sec
//currentOutgoingBitrate - kbits/sec
console.log("Average outgoing bitrate " + obj.averageOutgoingBitrate + " kbits/sec"
+ " Current outgoing bitrate: " + obj.currentOutgoingBitrate + " kbits/sec");
}
},
callbackError: function (error, message) {
//some of the possible errors, NotFoundError, SecurityError,PermissionDeniedError
console.log("error callback: " + JSON.stringify(error));
var errorMessage = JSON.stringify(error);
if (typeof message != "undefined") {
errorMessage = message;
}
if (error.indexOf("NotFoundError") != -1) {
errorMessage = "Camera or Mic are not found or not allowed in your device";
} else if (error.indexOf("NotReadableError") != -1 || error.indexOf("TrackStartError") != -1) {
errorMessage = "Camera or Mic is being used by some other process that does not let read the devices";
} else if (error.indexOf("OverconstrainedError") != -1 || error.indexOf("ConstraintNotSatisfiedError") != -1) {
errorMessage = "There is no device found that fits your video and audio constraints. You may change video and audio constraints"
} else if (error.indexOf("NotAllowedError") != -1 || error.indexOf("PermissionDeniedError") != -1) {
errorMessage = "You are not allowed to access camera and mic.";
} else if (error.indexOf("TypeError") != -1) {
errorMessage = "Video/Audio is required";
}
alert(errorMessage);
}
});
}
} else {
this.restartDevices()
}

1110
src/lib/webrtc_adaptor.js
File diff suppressed because it is too large
View File

13
src/router/index.js

@ -17,11 +17,24 @@ Vue.use(VueRouter)
path: '/rooms',
name: 'Room',
component: () => import(/* webpackChunkName: "Room" */ '../views/Room.vue')
},{
path: '/animator',
name: 'Animator',
component: () => import(/* webpackChunkName: "Animator" */ '../views/Animator.vue')
},
{
path: '/room/:id',
name: 'RoomView',
component: () => import(/* webpackChunkName: "RoomView" */ '../views/RoomView.vue')
},{
path: '/stream/:id/:name/:streamer',
name: 'Streamer',
component: () => import(/* webpackChunkName: "Streamer" */ '../views/RoomView.vue')
},
{
path: '/remote/:id/:user',
name: 'remote',
component: () => import(/* webpackChunkName: "remote" */ '../views/remote.vue')
}
]

720
src/views/Animator.vue

@ -0,0 +1,720 @@
<template>
<v-container>
<v-row class="fill-height">
<v-col cols="24" md="1">
<v-card width="100%" dark class="fill-height">
<v-card-text>
<v-row>
<v-col>
<v-icon @click="addItemStart('text')">mdi-format-color-text</v-icon>
</v-col>
<v-col>
<v-icon @mousedown="addItemStart('rect')" @mouseup="clearStart()">
mdi-rectangle-outline
</v-icon>
</v-col>
<v-col>
<v-icon @mousedown="addItemStart('image')" @mouseup="clearStart()">mdi-image</v-icon>
</v-col>
</v-row>
</v-card-text>
</v-card>
</v-col>
<v-col cols="24" md="9">
<v-card width="100%" min-height="600" dark class="fill-height">
<v-card-text color="white">
<div ref="vid" @click="handleAction" style="width: 100%;background: #fff;position: relative">
<div v-for="(track, index) in mainTracks" :key="index">
<div v-for="(keyframe, index) in track.keyframes" :key="index">
<div @click="selectTrack(track)"
v-if="keyframe.start <= currentTime && keyframe.end >=currentTime && track.type === 'text'"
@mouseenter="colorBorder(track)"
@mouseleave="colorBorderClear(track)" @mousedown="itemMoveStart"
@mouseup="itemMoveEnd" :ref="'item'+track.id" @dblclick="editText(track)"
:style="`padding:4px;color:${track.color};position:absolute;opacity:${vidTracks[track.id].opacity};top:${vidTracks[track.id].y-4}px;left:${vidTracks[track.id].x-4}px;font-size:${track.size};font-family:${track.font};border: 1px solid ${selectedTrack?(selectedTrack.id === track.id?'#ff0000':'transparent'):'transparent'}`"
>
<span :style="`line-height:${vidTracks[track.id].lineHeight}!important;letter-spacing:${vidTracks[track.id].letterSpacing}!important;`" v-html="track.text.replace(/\n/g, '<br>')"></span>
<div></div>
</div>
<div @click="selectTrack(track)" @mouseenter="colorBorder(track)"
v-if="keyframe.start <= currentTime && keyframe.end >=currentTime && track.type === 'image'"
@mouseleave="colorBorderClear(track)" @mousedown="itemMoveStart"
@mouseup="itemMoveEnd" :ref="'item'+track.id" @dblclick="editText(track)"
:style="`padding:4px;color:${track.color};position:absolute;opacity:${vidTracks[track.id].opacity};top:${vidTracks[track.id].y-4}px;left:${vidTracks[track.id].x-4}px;font-size:${track.size};font-family:${track.font};border: 1px solid ${selectedTrack?(selectedTrack.id === track.id?'#ff0000':'transparent'):'transparent'}`"
>
<img :src="track.image_data"
:width="vidTracks[track.id].width>0?vidTracks[track.id].width+'px':''"
:height="vidTracks[track.id].height>0?vidTracks[track.id].height+'px':''">
<div></div>
</div>
<div @click="selectTrack(track)" @mouseenter="colorBorder(track)"
v-if="keyframe.start <= currentTime && keyframe.end >=currentTime && track.type === 'rect'"
@mouseleave="colorBorderClear(track)" @mousedown="itemMoveStart"