Browse Source
Multiparty Jingle
Multiparty Jingle
- Add support of XEP-0482: Call Invites - Add support of XEP-0272: Multiparty Jingle - Create Muji tables in the database - Add Moxl Muji and Call Invites classes - Refactoring to allow multiple Jingles at the same time - Refactor the Jingle XMPP payload to use Packets - Generalize the from parameter in all the Jingle events - Create/delete dynamicaly the remote audio and video tags per Jingle session - Initiate individual Jingle sessions in a MujiCall - Refactor the presence handling to initiate properly the individual Jingle sessions - Add adjustments to fit with Dino - Move the audio/video toggle and screen sharing features to MovimJingles - Handle properly Call Invite retractation - Handle state where there is several Muji calls invites at the same time - Add Ongoing Call in the title bar - Handle mute/unmute in Muji calls - Create new status messages to track the calls in the MUCs - Complete DOAPpull/1368/head
87 changed files with 3427 additions and 1935 deletions
-
2CHANGELOG.md
-
16app/Conference.php
-
9app/Helpers/StringHelper.php
-
6app/Helpers/UtilsHelper.php
-
61app/MujiCall.php
-
34app/MujiCallParticipant.php
-
6app/Presence.php
-
5app/Session.php
-
66app/Widgets/Chat/Chat.php
-
51app/Widgets/Chat/_chat_header.tpl
-
1app/Widgets/Chat/_chat_muji_propose.tpl
-
1app/Widgets/Chat/_chat_muji_retract.tpl
-
2app/Widgets/Chat/chat.js
-
2app/Widgets/Chat/locales.ini
-
17app/Widgets/Chats/Chats.php
-
11app/Widgets/Chats/_chats_calls.tpl
-
5app/Widgets/Chats/chats.tpl
-
12app/Widgets/ContactActions/ContactActions.php
-
2app/Widgets/ContactActions/_contactactions_drawer.tpl
-
2app/Widgets/Notif/_notif.tpl
-
13app/Widgets/Notif/notif.js
-
6app/Widgets/Presence/Presence.php
-
4app/Widgets/Publish/Publish.php
-
2app/Widgets/PublishStories/PublishStories.php
-
51app/Widgets/Rooms/Rooms.php
-
17app/Widgets/Rooms/_rooms_room.tpl
-
2app/Widgets/Rooms/locales.ini
-
8app/Widgets/Rooms/rooms.css
-
10app/Widgets/Rooms/rooms.js
-
4app/Widgets/Search/Search.php
-
2app/Widgets/Search/_search_roster.tpl
-
6app/Widgets/Search/search.js
-
2app/Widgets/Stickers/_stickers_emojis.tpl
-
454app/Widgets/Visio/Visio.php
-
126app/Widgets/Visio/_visio_lobby.tpl
-
9app/Widgets/Visio/locales.ini
-
175app/Widgets/Visio/visio.css
-
436app/Widgets/Visio/visio.js
-
20app/Widgets/Visio/visio.tpl
-
183app/Widgets/Visio/visio_utils.js
-
61database/migrations/20250329110303_create_muji_calls_table.php
-
1850doap.xml
-
2locales/locales.ini
-
408public/scripts/movim_jingles.js
-
2public/scripts/movim_rpc.js
-
11public/scripts/movim_utils.js
-
290public/scripts/movim_visio.js
-
5public/theme/css/form.css
-
4src/Movim/Controller/Base.php
-
49src/Movim/CurrentCall.php
-
11src/Movim/Librairies/JingletoSDP.php
-
61src/Movim/Librairies/SDPtoJingle.php
-
8src/Moxl/API.php
-
2src/Moxl/Stanza/Confirm.php
-
19src/Moxl/Stanza/Jingle.php
-
76src/Moxl/Stanza/JingleCallInvite.php
-
99src/Moxl/Stanza/Message.php
-
23src/Moxl/Stanza/Muc.php
-
17src/Moxl/Stanza/Presence.php
-
4src/Moxl/Stanza/Stream.php
-
2src/Moxl/Utils.php
-
2src/Moxl/Xec/Action/Jingle/SessionAccept.php
-
2src/Moxl/Xec/Action/Jingle/SessionTerminate.php
-
18src/Moxl/Xec/Action/JingleCallInvite/Accept.php
-
33src/Moxl/Xec/Action/JingleCallInvite/Invite.php
-
18src/Moxl/Xec/Action/JingleCallInvite/Left.php
-
18src/Moxl/Xec/Action/JingleCallInvite/Reject.php
-
34src/Moxl/Xec/Action/JingleCallInvite/Retract.php
-
22src/Moxl/Xec/Action/Muc/CreateMujiRoom.php
-
40src/Moxl/Xec/Action/Presence/Muc.php
-
14src/Moxl/Xec/Action/Presence/Unavailable.php
-
7src/Moxl/Xec/Handler.php
-
25src/Moxl/Xec/Payload/CallInviteAccept.php
-
27src/Moxl/Xec/Payload/CallInviteLeft.php
-
64src/Moxl/Xec/Payload/CallInvitePropose.php
-
39src/Moxl/Xec/Payload/CallInviteRetract.php
-
5src/Moxl/Xec/Payload/Carbons.php
-
75src/Moxl/Xec/Payload/Jingle.php
-
5src/Moxl/Xec/Payload/JingleAccept.php
-
8src/Moxl/Xec/Payload/JingleProceed.php
-
3src/Moxl/Xec/Payload/JinglePropose.php
-
5src/Moxl/Xec/Payload/JingleReject.php
-
25src/Moxl/Xec/Payload/JingleRetract.php
-
4src/Moxl/Xec/Payload/Payload.php
-
20src/Moxl/Xec/Payload/Presence.php
-
2src/Moxl/Xec/Payload/SASL2Challenge.php
-
2src/Moxl/Xec/Payload/SASLChallenge.php
@ -0,0 +1,61 @@ |
|||
<?php |
|||
|
|||
namespace App; |
|||
|
|||
use Awobaz\Compoships\Database\Eloquent\Model; |
|||
|
|||
class MujiCall extends Model |
|||
{ |
|||
use \Awobaz\Compoships\Compoships; |
|||
|
|||
public $incrementing = false; |
|||
protected $primaryKey = ['session_id', 'id']; |
|||
protected $fillable = ['id', 'muc', 'jidfrom', 'video', 'isfromconference']; |
|||
protected $with = ['participants', 'presences']; |
|||
|
|||
protected $attributes = [ |
|||
'session_id' => SESSION_ID |
|||
]; |
|||
|
|||
public function session() |
|||
{ |
|||
return $this->hasOne('App\Session'); |
|||
} |
|||
|
|||
public function conference() |
|||
{ |
|||
return $this->hasOne('App\Conference', 'conference', 'jidfrom') |
|||
->where('session_id', $this->session_id); |
|||
} |
|||
|
|||
public function presences() |
|||
{ |
|||
return $this->hasMany('App\Presence', 'jid', 'muc') |
|||
->where('session_id', $this->session_id) |
|||
->where('value', '<', '5') |
|||
->where('resource', '!=', ''); |
|||
} |
|||
|
|||
public function presence() |
|||
{ |
|||
return $this->hasOne('App\Presence', ['jid', 'session_id'], ['muc', 'session_id']) |
|||
->where('value', '<', 5) |
|||
->where('mucjid', \App\User::me()->id); |
|||
} |
|||
|
|||
public function participants() |
|||
{ |
|||
return $this->hasMany('App\MujiCallParticipant', 'muji_call_id', 'id') |
|||
->where('session_id', $this->session_id); |
|||
} |
|||
|
|||
public function getIconAttribute() |
|||
{ |
|||
return $this->video ? 'videocam' : 'call'; |
|||
} |
|||
|
|||
public function getendIconAttribute() |
|||
{ |
|||
return $this->video ? 'videocam_off' : 'call_end'; |
|||
} |
|||
} |
|||
@ -0,0 +1,34 @@ |
|||
<?php |
|||
|
|||
namespace App; |
|||
|
|||
use Movim\Image; |
|||
use Movim\Model; |
|||
|
|||
class MujiCallParticipant extends Model |
|||
{ |
|||
public $incrementing = false; |
|||
protected $primaryKey = ['session_id', 'muji_call_id', 'jid']; |
|||
protected $fillable = ['session_id', 'muji_call_id', 'jid', 'left_at', 'inviter']; |
|||
|
|||
protected $attributes = [ |
|||
'session_id' => SESSION_ID |
|||
]; |
|||
|
|||
public function session() |
|||
{ |
|||
return $this->hasOne('App\Session'); |
|||
} |
|||
|
|||
public function mujiCall() |
|||
{ |
|||
return $this->belongsTo('App\MujiCall', 'id', 'muji_call_id') |
|||
|
|||
->where('session_id', $this->session_id); |
|||
} |
|||
|
|||
public function getConferencePictureAttribute(): string |
|||
{ |
|||
return Image::getOrCreate($this->jid, 120) ?? avatarPlaceholder($this->jid . 'groupchat'); |
|||
} |
|||
} |
|||
@ -0,0 +1 @@ |
|||
<i class="material-symbols icon green">call</i> {$c->__('chat.muji_propose')} |
|||
@ -0,0 +1 @@ |
|||
<i class="material-symbols icon red">call_end</i> {$c->__('chat.muji_retract')}{if="$diff"} • {if="$diff->h > 0"}{$c->__('chat.jingle_hours', $diff->h, $diff->i)}{elseif="$diff->i > 0"}{$c->__('chat.jingle_minutes', $diff->i, $diff->s)}{else}{$c->__('chat.jingle_seconds', $diff->s)}{/if}{/if} |
|||
@ -0,0 +1,11 @@ |
|||
{loop="$calls"} |
|||
<li> |
|||
<span class="primary icon gray"> |
|||
<i class="material-symbols">interpreter_mode</i> |
|||
</span> |
|||
<div> |
|||
<p>{$value->id}</p> |
|||
<p><i class="material-symbols icon green blink">phone_in_talk</i> {$c->__('visio.in_call')}</p> |
|||
</div> |
|||
</li> |
|||
{/loop} |
|||
@ -1,436 +0,0 @@ |
|||
function logError(error) { |
|||
console.log(error.name + ': ' + error.message); |
|||
console.error(error); |
|||
} |
|||
|
|||
var Visio = { |
|||
calling: false, |
|||
|
|||
videoSelect: undefined, |
|||
switchCamera: undefined, |
|||
|
|||
inboundStream: null, |
|||
|
|||
states: null, |
|||
|
|||
tracksTypes: [], |
|||
|
|||
prepare: function (from, id, withVideo) { |
|||
if (!MovimVisio.localStream) return; |
|||
|
|||
Visio_ajaxPrepare(from); |
|||
|
|||
MovimVisio.from = from; |
|||
MovimVisio.id = id; |
|||
MovimVisio.withVideo = withVideo ?? false; |
|||
}, |
|||
|
|||
init: function (bareFrom) { |
|||
let visio = document.querySelector('#visio'); |
|||
|
|||
visio.dataset.from = bareFrom; |
|||
|
|||
delete visio.dataset.type; |
|||
visio.dataset.type = (MovimVisio.withVideo) ? 'video' : 'audio'; |
|||
|
|||
MovimVisio.load(); |
|||
|
|||
MovimVisio.pc = new RTCPeerConnection({ 'iceServers': MovimVisio.services }); |
|||
|
|||
MovimVisio.pc.ontrack = event => { |
|||
var srcObject = null; |
|||
|
|||
if (event.streams && event.streams[0]) { |
|||
srcObject = event.streams[0]; |
|||
} else { |
|||
if (!MovimVisio.inboundStream) { |
|||
MovimVisio.inboundStream = new MediaStream(); |
|||
MovimVisio.remoteAudio.srcObject = MovimVisio.inboundStream; |
|||
} |
|||
|
|||
MovimVisio.inboundStream.addTrack(event.track); |
|||
srcObject = MovimVisio.inboundStream; |
|||
} |
|||
|
|||
VisioUtils.setRemoteVideoState(''); |
|||
|
|||
if (event.track.kind == 'audio') { |
|||
MovimVisio.remoteAudio.srcObject = srcObject; |
|||
VisioUtils.setRemoteAudioState('mic'); |
|||
} else if (event.track.kind == 'video') { |
|||
MovimVisio.remoteVideo.srcObject = srcObject; |
|||
VisioUtils.setRemoteVideoState('videocam'); |
|||
} |
|||
|
|||
VisioUtils.handleRemoteAudio(); |
|||
Visio.tracksTypes['mid' + event.transceiver.mid] = event.track.kind; |
|||
}; |
|||
|
|||
MovimVisio.pc.onicecandidate = event => { |
|||
let candidate = event.candidate; |
|||
if (candidate && candidate.candidate && candidate.candidate.length > 0) { |
|||
Visio_ajaxCandidate(event.candidate, MovimVisio.from, MovimVisio.id); |
|||
} |
|||
}; |
|||
|
|||
MovimVisio.pc.oniceconnectionstatechange = () => VisioUtils.toggleMainButton(); |
|||
|
|||
MovimVisio.pc.onicegatheringstatechange = function (event) { |
|||
// When we didn't receive the WebRTC termination before Jingle
|
|||
if (MovimVisio.pc.iceConnectionState == 'disconnected') { |
|||
Visio.onTerminate(); |
|||
} |
|||
|
|||
VisioUtils.toggleMainButton(); |
|||
}; |
|||
|
|||
if (MovimVisio.withVideo) { |
|||
VisioUtils.pcReplaceTrack(MovimVisio.localStream); |
|||
} |
|||
|
|||
VisioUtils.toggleMainButton(); |
|||
|
|||
MovimVisio.localStream.getTracks().forEach(track => { |
|||
MovimVisio.pc.addTrack(track, MovimVisio.localStream); |
|||
}); |
|||
|
|||
if (MovimVisio.withVideo) { |
|||
VisioUtils.switchCameraInCall(); |
|||
} |
|||
|
|||
if (MovimVisio.id) { |
|||
Visio_ajaxAccept(MovimVisio.from, MovimVisio.id); |
|||
} else { |
|||
MovimVisio.id = crypto.randomUUID(); |
|||
Visio.calling = true; |
|||
VisioUtils.toggleMainButton(); |
|||
Visio_ajaxPropose(MovimVisio.from, MovimVisio.id, MovimVisio.withVideo); |
|||
} |
|||
}, |
|||
|
|||
onMute: function (name) { |
|||
if (Visio.tracksTypes[name]) { |
|||
if (Visio.tracksTypes[name] == 'audio') { |
|||
VisioUtils.setRemoteAudioState('mic_off'); |
|||
} |
|||
|
|||
if (Visio.tracksTypes[name] == 'video') { |
|||
document.querySelector('#remote_video').classList.add('muted'); |
|||
VisioUtils.setRemoteVideoState('videocam_off'); |
|||
} |
|||
} |
|||
}, |
|||
|
|||
onUnmute: function (name) { |
|||
if (Visio.tracksTypes[name]) { |
|||
if (Visio.tracksTypes[name] == 'audio') { |
|||
VisioUtils.setRemoteAudioState('mic'); |
|||
} |
|||
|
|||
if (Visio.tracksTypes[name] == 'video') { |
|||
document.querySelector('#remote_video').classList.remove('muted'); |
|||
VisioUtils.setRemoteVideoState('videocam'); |
|||
} |
|||
} |
|||
}, |
|||
|
|||
setServices: function (services) { |
|||
MovimVisio.services = services; |
|||
}, |
|||
|
|||
setStates: function (states) { |
|||
Visio.states = states; |
|||
}, |
|||
|
|||
lobbySetup: function (withVideo) { |
|||
Visio.getUserMedia(withVideo); |
|||
}, |
|||
|
|||
getUserMedia: function(withVideo) { |
|||
var constraints = { |
|||
audio: true, |
|||
video: false, |
|||
}; |
|||
|
|||
if (withVideo) { |
|||
constraints.video = { |
|||
facingMode: 'user', |
|||
width: { ideal: 4096 }, |
|||
height: { ideal: 4096 } |
|||
} |
|||
|
|||
if (localStorage.defaultCamera) { |
|||
constraints.video = { |
|||
deviceId: localStorage.defaultCamera |
|||
}; |
|||
} |
|||
} |
|||
|
|||
if (localStorage.defaultMicrophone) { |
|||
constraints.audio = { |
|||
deviceId: localStorage.defaultMicrophone |
|||
} |
|||
} |
|||
|
|||
MovimVisio.load(); |
|||
|
|||
let lobby = document.querySelector('#visio_lobby'); |
|||
|
|||
if (lobby) { |
|||
VisioUtils.disableLobbyCallButton(); |
|||
} |
|||
|
|||
navigator.mediaDevices.getUserMedia(constraints).then(stream => { |
|||
MovimVisio.localStream = stream; |
|||
|
|||
if (lobby) { |
|||
lobby.classList.add('configure'); |
|||
} else { |
|||
MovimVisio.clear(); |
|||
return; |
|||
} |
|||
|
|||
stream.getTracks().forEach(track => { |
|||
if (lobby) { |
|||
VisioUtils.enableLobbyCallButton(); |
|||
} |
|||
|
|||
if (track.kind == 'audio') { |
|||
MovimVisio.localAudio.srcObject = stream; |
|||
} else if (withVideo && track.kind == 'video') { |
|||
MovimVisio.localVideo.srcObject = stream; |
|||
|
|||
if (lobby) { |
|||
let cameraPreview = lobby.querySelector('video#camera_preview'); |
|||
cameraPreview.addEventListener('loadeddata', () => cameraPreview.play()); |
|||
cameraPreview.srcObject = stream; |
|||
cameraPreview.disablePictureInPicture = true; |
|||
} |
|||
|
|||
} |
|||
}); |
|||
|
|||
VisioUtils.handleAudio(); |
|||
|
|||
if (withVideo) { |
|||
VisioUtils.enableScreenSharingButton(); |
|||
} |
|||
|
|||
navigator.mediaDevices.enumerateDevices().then(devices => Visio.gotDevices(withVideo, devices)); |
|||
}); |
|||
}, |
|||
|
|||
gotDevices: function(withVideo, devicesInfo) { |
|||
microphoneFound = false; |
|||
cameraFound = false; |
|||
|
|||
let microphoneSelect = document.querySelector('select[name=default_microphone]'); |
|||
microphoneSelect.onchange = (e) => { |
|||
localStorage.defaultMicrophone = e.target.value; |
|||
Visio.getUserMedia(withVideo); |
|||
}; |
|||
microphoneSelect.innerText = ''; |
|||
|
|||
VisioUtils.handleAudio(); |
|||
|
|||
let cameraSelect = document.querySelector('select[name=default_camera]'); |
|||
|
|||
if (cameraSelect) { |
|||
cameraSelect.addEventListener('change', e => { |
|||
localStorage.defaultCamera = e.target.value; |
|||
|
|||
let cameraPreview = document.querySelector('video#camera_preview'); |
|||
|
|||
if (cameraPreview.srcObject) { |
|||
cameraPreview.srcObject.getTracks().forEach(track => track.stop()); |
|||
} |
|||
|
|||
cameraPreview.srcObject = null; |
|||
|
|||
Visio.getUserMedia(withVideo); |
|||
}); |
|||
cameraSelect.innerText = ''; |
|||
} |
|||
|
|||
for (const deviceInfo of devicesInfo) { |
|||
if (deviceInfo.kind === 'audioinput') { |
|||
const option = document.createElement('option'); |
|||
option.value = deviceInfo.deviceId; |
|||
option.text = deviceInfo.label || `Microphone ${microphoneSelect.length + 1}`; |
|||
|
|||
if (deviceInfo.deviceId == localStorage.defaultMicrophone) { |
|||
option.selected = true; |
|||
microphoneFound = true; |
|||
} |
|||
|
|||
microphoneSelect.appendChild(option); |
|||
} |
|||
|
|||
if (withVideo && deviceInfo.kind === 'videoinput') { |
|||
const option = document.createElement('option'); |
|||
option.value = deviceInfo.deviceId; |
|||
option.text = deviceInfo.label || `Camera ${microphoneSelect.length + 1}`; |
|||
|
|||
if (deviceInfo.deviceId == localStorage.defaultCamera) { |
|||
option.selected = true; |
|||
cameraFound = true; |
|||
} |
|||
|
|||
// Sometimes we can have two devices with the same id
|
|||
if (!cameraSelect.querySelector('option[value="' + deviceInfo.deviceId + '"]')) { |
|||
cameraSelect.appendChild(option); |
|||
} |
|||
} |
|||
} |
|||
|
|||
if (microphoneFound == false) { |
|||
localStorage.defaultMicrophone = microphoneSelect.value; |
|||
} |
|||
|
|||
if (withVideo && cameraFound == false) { |
|||
localStorage.defaultCamera = cameraSelect.value; |
|||
} |
|||
}, |
|||
|
|||
gotQuickStream: function () { |
|||
VisioUtils.pcReplaceTrack(MovimVisio.localVideo.srcObject); |
|||
}, |
|||
|
|||
gotScreen: function () { |
|||
VisioUtils.pcReplaceTrack(MovimVisio.screenSharing.srcObject); |
|||
}, |
|||
|
|||
onCandidate: function (candidate, mid, mlineindex) { |
|||
// filter the a=candidate lines
|
|||
var filtered = candidate.split(/\n/).filter(line => { |
|||
return line.startsWith('a=candidate'); |
|||
}); |
|||
|
|||
MovimVisio.pc.addIceCandidate(new RTCIceCandidate({ |
|||
'candidate': filtered.join('').substring(2), |
|||
'sdpMid': mid, |
|||
'sdpMLineIndex': mlineindex |
|||
}), () => { }, logError); |
|||
}, |
|||
|
|||
onProceed: function (from, id) { |
|||
if (from.substring(0, MovimVisio.from.length) == MovimVisio.from && MovimVisio.id == id) { |
|||
// We set the remote resource
|
|||
MovimVisio.from = from; |
|||
|
|||
MovimVisio.pc.createOffer().then(function (offer) { |
|||
Visio.calling = false; |
|||
VisioUtils.toggleMainButton(); |
|||
return MovimVisio.pc.setLocalDescription(offer); |
|||
}) |
|||
.then(function () { |
|||
Visio_ajaxSessionInitiate(MovimVisio.pc.localDescription, MovimVisio.from, MovimVisio.id); |
|||
}); |
|||
} else { |
|||
console.error('Wrong call') |
|||
} |
|||
}, |
|||
|
|||
onInitiateSDP: function (sdp) { |
|||
MovimVisio.pc.setRemoteDescription(new RTCSessionDescription({ 'sdp': sdp + "\n", 'type': 'offer' }), () => { |
|||
MovimVisio.pc.createAnswer().then(function (answer) { |
|||
return MovimVisio.pc.setLocalDescription(answer); |
|||
}).then(function () { |
|||
Visio_ajaxSessionAccept(MovimVisio.pc.localDescription, MovimVisio.from, MovimVisio.id); |
|||
}).catch(logError); |
|||
}, logError); |
|||
}, |
|||
|
|||
onContentAdd: function (sdp) { |
|||
MovimVisio.pc.setRemoteDescription(new RTCSessionDescription({ 'sdp': sdp + "\n", 'type': 'offer' }), () => { |
|||
}, logError); |
|||
}, |
|||
|
|||
onAcceptSDP: function (sdp) { |
|||
MovimVisio.pc.setRemoteDescription( |
|||
new RTCSessionDescription({ 'sdp': sdp + "\n", 'type': 'answer' }), () => { }, |
|||
(error) => { |
|||
Visio.goodbye('incompatible-parameters'); |
|||
logError(error) |
|||
} |
|||
); |
|||
}, |
|||
|
|||
onTerminate: (reason) => { |
|||
if (MovimVisio.localAudio) { |
|||
let localStream = MovimVisio.localAudio.srcObject; |
|||
|
|||
if (localStream) { |
|||
localStream.getTracks().forEach(track => track.stop()); |
|||
} |
|||
|
|||
MovimVisio.localAudio.srcObject = null; |
|||
} |
|||
|
|||
if (MovimVisio.remoteAudio) { |
|||
let remoteStream = MovimVisio.remoteAudio.srcObject; |
|||
|
|||
if (remoteStream) { |
|||
remoteStream.getTracks().forEach(track => track.stop()); |
|||
} |
|||
|
|||
MovimVisio.remoteAudio.srcObject = null; |
|||
} |
|||
|
|||
if (MovimVisio.localVideo) { |
|||
let localStream = MovimVisio.localVideo.srcObject; |
|||
|
|||
if (localStream) { |
|||
localStream.getTracks().forEach(track => track.stop()); |
|||
} |
|||
|
|||
MovimVisio.localVideo.srcObject = null; |
|||
} |
|||
|
|||
if (MovimVisio.remoteVideo) { |
|||
let remoteStream = MovimVisio.remoteVideo.srcObject; |
|||
|
|||
if (remoteStream) { |
|||
remoteStream.getTracks().forEach(track => track.stop()); |
|||
} |
|||
|
|||
MovimVisio.remoteVideo.srcObject = null; |
|||
} |
|||
|
|||
if (VisioUtils.audioContext) { |
|||
VisioUtils.audioContext.close(); |
|||
VisioUtils.audioContext = null; |
|||
} |
|||
|
|||
if (VisioUtils.remoteAudioContext) { |
|||
VisioUtils.remoteAudioContext.close(); |
|||
VisioUtils.remoteAudioContext = null; |
|||
} |
|||
}, |
|||
|
|||
goodbye: (reason) => { |
|||
Visio.onTerminate(reason); |
|||
|
|||
let visio = document.querySelector('#visio'); |
|||
delete visio.dataset.from; |
|||
delete visio.dataset.type; |
|||
|
|||
if (document.fullscreenElement) { |
|||
document.exitFullscreen(); |
|||
} |
|||
|
|||
if (MovimVisio.id) { |
|||
Visio_ajaxEnd(MovimVisio.from, MovimVisio.id, reason); |
|||
} |
|||
|
|||
MovimVisio.clear(); |
|||
}, |
|||
} |
|||
|
|||
MovimWebsocket.attach(() => { |
|||
if (MovimVisio.services.length == 0) { |
|||
Visio_ajaxResolveServices(); |
|||
} |
|||
|
|||
Visio_ajaxGetStates(); |
|||
}); |
|||
@ -0,0 +1,61 @@ |
|||
<?php |
|||
|
|||
use Movim\Migration; |
|||
use Illuminate\Database\Schema\Blueprint; |
|||
|
|||
class CreateMujiCallsTable extends Migration |
|||
{ |
|||
public function up() |
|||
{ |
|||
$this->schema->create('muji_calls', function (Blueprint $table) { |
|||
$table->string('id', 256); |
|||
$table->string('muc', 256); |
|||
$table->string('session_id', 64); |
|||
$table->string('jidfrom', 128); |
|||
$table->boolean('isfromconference')->default(false); |
|||
$table->boolean('video', false); |
|||
|
|||
$table->foreign('session_id') |
|||
->references('id')->on('sessions') |
|||
->onDelete('cascade'); |
|||
|
|||
$table->primary(['id', 'session_id']); |
|||
|
|||
$table->timestamps(); |
|||
}); |
|||
|
|||
$this->schema->create('muji_call_participants', function (Blueprint $table) { |
|||
$table->string('session_id', 64); |
|||
$table->string('muji_call_id', 256); |
|||
$table->string('jid', 256); |
|||
$table->boolean('inviter')->default(false); |
|||
$table->dateTime('left_at')->nullable(); |
|||
|
|||
$table->primary(['session_id', 'muji_call_id', 'jid']); |
|||
|
|||
$table->foreign('session_id') |
|||
->references('id')->on('sessions') |
|||
->onDelete('cascade'); |
|||
|
|||
$table->foreign(['session_id', 'muji_call_id']) |
|||
->references(['session_id', 'id'])->on('muji_calls') |
|||
->onDelete('cascade'); |
|||
|
|||
$table->timestamps(); |
|||
}); |
|||
|
|||
$this->schema->table('presences', function (Blueprint $table) { |
|||
$table->string('mucjidresource')->nullable(); |
|||
}); |
|||
} |
|||
|
|||
public function down() |
|||
{ |
|||
$this->schema->drop('muji_call_participants'); |
|||
$this->schema->drop('muji_calls'); |
|||
|
|||
$this->schema->table('presences', function (Blueprint $table) { |
|||
$table->dropColumn('mucjidresource'); |
|||
}); |
|||
} |
|||
} |
|||
1850
doap.xml
File diff suppressed because it is too large
View File
File diff suppressed because it is too large
View File
@ -0,0 +1,408 @@ |
|||
var MovimJingleSession = function (jid, fullJid, id, name, avatarUrl) { |
|||
this.id = id ?? crypto.randomUUID(); |
|||
this.jid = jid; |
|||
this.fullJid = fullJid; |
|||
this.tracksTypes = {}; |
|||
this.name = name; |
|||
this.avatarUrl = avatarUrl; |
|||
|
|||
this.pc = new RTCPeerConnection({ 'iceServers': MovimVisio.services }); |
|||
|
|||
this.participant = document.createElement('div'); |
|||
this.participant.dataset.jid = this.jid; |
|||
this.participant.classList.add('participant'); |
|||
document.querySelector('#participants').appendChild(this.participant); |
|||
this.participant.classList.add('video_off'); |
|||
this.participant.classList.add('audio_off'); |
|||
|
|||
this.remoteVideo = document.createElement('video'); |
|||
this.remoteVideo.autoplay = true; |
|||
this.remoteVideo.disablePictureInPicture = true; |
|||
this.remoteVideo.poster = BASE_URI + 'theme/img/empty.png'; |
|||
this.participant.appendChild(this.remoteVideo); |
|||
|
|||
this.remoteAudio = document.createElement('audio'); |
|||
this.remoteAudio.autoplay = true; |
|||
this.participant.appendChild(this.remoteAudio); |
|||
|
|||
if (this.name) { |
|||
this.participant.dataset.name = this.name; |
|||
} |
|||
|
|||
if (this.avatarUrl) { |
|||
let background = document.createElement('img'); |
|||
background.classList.add('avatar'); |
|||
background.src = this.avatarUrl; |
|||
this.participant.appendChild(background); |
|||
} |
|||
|
|||
this.pc.ontrack = event => { |
|||
var srcObject = null; |
|||
|
|||
if (event.streams && event.streams[0]) { |
|||
srcObject = event.streams[0]; |
|||
} else { |
|||
if (!this.inboundStream) { |
|||
this.inboundStream = new MediaStream(); |
|||
this.remoteAudio.srcObject = this.inboundStream; |
|||
} |
|||
|
|||
this.inboundStream.addTrack(event.track); |
|||
srcObject = this.inboundStream; |
|||
} |
|||
|
|||
if (event.track.kind == 'audio') { |
|||
this.remoteAudio.srcObject = srcObject; |
|||
this.participant.classList.remove('audio_off'); |
|||
} else if (event.track.kind == 'video') { |
|||
this.remoteVideo.srcObject = srcObject; |
|||
this.participant.classList.remove('video_off'); |
|||
} |
|||
|
|||
this.tracksTypes['mid' + event.transceiver.mid] = event.track.kind; |
|||
|
|||
this.handleRemoteAudio(); |
|||
} |
|||
|
|||
this.pc.onicecandidate = event => { |
|||
let candidate = event.candidate; |
|||
|
|||
if (candidate && candidate.candidate && candidate.candidate.length > 0) { |
|||
Visio_ajaxCandidate(this.fullJid, this.id, event.candidate); |
|||
} |
|||
}; |
|||
|
|||
MovimVisio.localStream.getTracks().forEach(track => { |
|||
this.pc.addTrack(track, MovimVisio.localStream); |
|||
}); |
|||
} |
|||
|
|||
MovimJingleSession.prototype.handleRemoteAudio = function () { |
|||
this.remoteAudioContext = new AudioContext(); |
|||
|
|||
try { |
|||
var remoteMicrophone = this.remoteAudioContext.createMediaStreamSource( |
|||
this.remoteAudio.srcObject |
|||
); |
|||
} catch (error) { |
|||
MovimUtils.logError(error); |
|||
return; |
|||
} |
|||
|
|||
var remoteJavascriptNode = this.remoteAudioContext.createScriptProcessor(2048, 1, 1); |
|||
this.isMuteStep = 0; |
|||
this.remoteMaxLevel = 0; |
|||
|
|||
remoteMicrophone.connect(remoteJavascriptNode); |
|||
remoteJavascriptNode.connect(this.remoteAudioContext.destination); |
|||
remoteJavascriptNode.onaudioprocess = (event) => { |
|||
var inpt = event.inputBuffer.getChannelData(0); |
|||
var instant = 0.0; |
|||
var sum = 0.0; |
|||
|
|||
for (var i = 0; i < inpt.length; ++i) { |
|||
sum += inpt[i] * inpt[i]; |
|||
} |
|||
|
|||
instant = Math.sqrt(sum / inpt.length); |
|||
this.remoteMaxLevel = Math.max(this.remoteMaxLevel, instant); |
|||
|
|||
var base = (instant / this.remoteMaxLevel); |
|||
var level = (base > 0.05) ? base ** .3 : 0; |
|||
|
|||
// Fallback in case we don't have the proper signalisation
|
|||
if (level == 0) { |
|||
this.isMuteStep++; |
|||
} else { |
|||
this.isMuteStep = 0; |
|||
} |
|||
|
|||
if (this.isMuteStep > 250) { |
|||
this.remoteVideo.classList.add('audio_off'); |
|||
} else { |
|||
this.remoteVideo.classList.remove('audio_off'); |
|||
} |
|||
|
|||
this.participant.style.setProperty('--level', level.toFixed(2)); |
|||
} |
|||
} |
|||
|
|||
MovimJingleSession.prototype.terminate = function (reason) { |
|||
Visio_ajaxTerminate(this.fullJid, this.id, reason); |
|||
this.close(); |
|||
} |
|||
|
|||
MovimJingleSession.prototype.close = function () { |
|||
if (this.pc) { |
|||
this.pc.close(); |
|||
this.pc = null; |
|||
} |
|||
|
|||
if (this.remoteAudioContext) { |
|||
this.remoteAudioContext.close(); |
|||
this.remoteAudioContext = null; |
|||
} |
|||
|
|||
let participant = document.querySelector('.participant[data-jid="' + this.jid + '"]'); |
|||
if (participant) participant.remove(); |
|||
|
|||
let remoteVideoStream = this.remoteVideo.srcObject; |
|||
if (remoteVideoStream) { |
|||
remoteVideoStream.getTracks().forEach(track => track.stop()); |
|||
remoteVideoStream = null; |
|||
} |
|||
|
|||
let remoteAudioStream = this.remoteAudio.srcObject; |
|||
if (remoteAudioStream) { |
|||
remoteAudioStream.getTracks().forEach(track => track.stop()); |
|||
remoteAudioStream = null; |
|||
} |
|||
} |
|||
|
|||
MovimJingleSession.prototype.onCandidate = function (candidate, mid, mlineindex) { |
|||
this.pc.addIceCandidate(new RTCIceCandidate({ |
|||
// filter the a=candidate lines
|
|||
'candidate': candidate.split(/\n/).filter(line => { |
|||
return line.startsWith('a=candidate'); |
|||
}).join('').substring(2), |
|||
'sdpMid': mid, |
|||
'sdpMLineIndex': mlineindex |
|||
})).catch(error => MovimUtils.logError(error)); |
|||
} |
|||
|
|||
MovimJingleSession.prototype.onContentAdd = function (sdp) { |
|||
this.pc.setRemoteDescription(new RTCSessionDescription({ 'sdp': sdp + "\n", 'type': 'offer' })) |
|||
.catch(error => MovimUtils.logError(error)); |
|||
} |
|||
|
|||
MovimJingleSession.prototype.sessionInitiate = function (fullJid, id, mujiRoom) { |
|||
this.id = id; |
|||
this.fullJid = fullJid; |
|||
this.pc.createOffer() |
|||
.then(offer => this.pc.setLocalDescription(offer)) |
|||
.then(() => Visio_ajaxSessionInitiate(this.fullJid, this.pc.localDescription, id, mujiRoom)); |
|||
} |
|||
|
|||
MovimJingleSession.prototype.onAcceptSDP = function (sdp) { |
|||
this.pc.setRemoteDescription(new RTCSessionDescription({ 'sdp': sdp + "\n", 'type': 'answer' })) |
|||
.catch(error => { |
|||
this.terminate('incompatible-parameters'); |
|||
MovimUtils.logError(error) |
|||
}); |
|||
} |
|||
|
|||
MovimJingleSession.prototype.onInitiateSDP = function (sdp) { |
|||
this.pc.setRemoteDescription(new RTCSessionDescription({ 'sdp': sdp + "\n", 'type': 'offer' })) |
|||
.then(() => { |
|||
this.pc.createAnswer() |
|||
.then(answer => this.pc.setLocalDescription(answer)) |
|||
.then(() => Visio_ajaxSessionAccept(this.fullJid, this.id, this.pc.localDescription)) |
|||
.catch(MovimUtils.logError); |
|||
}).catch(error => MovimUtils.logError(error)); |
|||
} |
|||
|
|||
MovimJingleSession.prototype.onReplaceTrack = function (videoTrack) { |
|||
var sender = this.pc.getSenders().find(s => s.track && s.track.kind == videoTrack.kind); |
|||
|
|||
if (sender) { |
|||
sender.replaceTrack(videoTrack); |
|||
} |
|||
} |
|||
|
|||
MovimJingleSession.prototype.enableTrack = function (enable = true, kind) { |
|||
let rtc = this.pc.getSenders().find(rtc => rtc.track && rtc.track.kind == kind); |
|||
let mid = this.pc.getTransceivers().filter(t => t.sender.track.id == rtc.track.id)[0].mid; |
|||
|
|||
if (rtc) { |
|||
if (enable) { |
|||
Visio_ajaxMute(this.fullJid, this.id, 'mid' + mid); |
|||
} else { |
|||
Visio_ajaxUnmute(this.fullJid, this.id, 'mid' + mid); |
|||
} |
|||
} |
|||
} |
|||
|
|||
MovimJingleSession.prototype.onMute = function (name) { |
|||
if (this.tracksTypes[name]) { |
|||
if (this.tracksTypes[name] == 'audio') { |
|||
this.participant.classList.add('audio_off'); |
|||
} |
|||
|
|||
if (this.tracksTypes[name] == 'video') { |
|||
this.participant.classList.add('video_off'); |
|||
} |
|||
} |
|||
} |
|||
|
|||
MovimJingleSession.prototype.onUnmute = function (name) { |
|||
if (this.tracksTypes[name]) { |
|||
if (this.tracksTypes[name] == 'audio') { |
|||
this.participant.classList.remove('audio_off'); |
|||
} |
|||
|
|||
if (this.tracksTypes[name] == 'video') { |
|||
this.participant.classList.remove('video_off'); |
|||
} |
|||
} |
|||
} |
|||
|
|||
MovimJingleSession.prototype.replaceLocalStream = function (stream) { |
|||
let videoTrack = stream.getVideoTracks()[0]; |
|||
var sender = this.pc.getSenders().find(s => s.track && videoTrack && s.track.kind == videoTrack.kind); |
|||
|
|||
if (sender) { |
|||
sender.replaceTrack(videoTrack); |
|||
} else { |
|||
stream.getTracks().forEach(track => { |
|||
this.pc.addTrack(track, stream); |
|||
}); |
|||
} |
|||
} |
|||
|
|||
var MovimJingles = { |
|||
sessions: {}, |
|||
|
|||
startCalls: function (mujiRoom) { |
|||
for (jid of Object.keys(MovimJingles.sessions)) { |
|||
MovimJingles.onProceed(jid, MovimJingles.sessions[jid].fullJid, MovimJingles.sessions[jid].id, mujiRoom); |
|||
} |
|||
}, |
|||
|
|||
initSession: function (jid, fullJid, id, name, avatarUrl) { |
|||
if (Object.keys(MovimJingles.sessions).includes(jid)) { |
|||
return; |
|||
} |
|||
|
|||
if (!MovimVisio.localStream) { |
|||
throw Error('localStream is not ready'); |
|||
}; |
|||
|
|||
MovimJingles.sessions[jid] = new MovimJingleSession(jid, fullJid, id, name, avatarUrl); |
|||
return MovimJingles.sessions[jid].id; |
|||
}, |
|||
|
|||
enableAudio: function (enable = true) { |
|||
MovimVisio.localStream.getTracks().filter(track => track.kind == 'audio').forEach(track => { |
|||
track.enabled = enable; |
|||
}); |
|||
|
|||
for (jid of Object.keys(MovimJingles.sessions)) { |
|||
MovimJingles.sessions[jid].enableTrack(enable, 'audio'); |
|||
} |
|||
}, |
|||
|
|||
enableVideo: function (enable = true) { |
|||
MovimVisio.localStream.getTracks().filter(track => track.kind == 'video').forEach(track => { |
|||
track.enabled = enable; |
|||
}); |
|||
|
|||
for (jid of Object.keys(MovimJingles.sessions)) { |
|||
MovimJingles.sessions[jid].enableTrack(enable, 'video'); |
|||
} |
|||
}, |
|||
|
|||
replaceLocalStream: function (stream) { |
|||
for (session of Object.values(MovimJingles.sessions)) { |
|||
session.replaceLocalStream(stream); |
|||
} |
|||
}, |
|||
|
|||
onCandidate: function (jid, candidate, mid, mlineindex) { |
|||
if (MovimJingles.sessions[jid] == undefined) { |
|||
throw Error('Candidate from a non initiated session ' + jid); |
|||
} |
|||
|
|||
MovimJingles.sessions[jid].onCandidate(candidate, mid, mlineindex); |
|||
}, |
|||
|
|||
onInitiateSDP: function (jid, sdp, sid) { |
|||
if (MovimJingles.sessions[jid] == undefined) { |
|||
throw Error('Initiate SDP from a non initiated session ' + jid); |
|||
} |
|||
|
|||
// Put the real sid
|
|||
MovimJingles.sessions[jid].id = sid; |
|||
MovimJingles.sessions[jid].onInitiateSDP(sdp); |
|||
}, |
|||
|
|||
onAcceptSDP: function (jid, sdp) { |
|||
if (MovimJingles.sessions[jid] == undefined) { |
|||
throw Error('Accept SDP from a non initiated session ' + jid); |
|||
} |
|||
|
|||
MovimJingles.sessions[jid].onAcceptSDP(sdp); |
|||
}, |
|||
|
|||
onProceed: function (jid, fullJid, id, mujiRoom) { |
|||
if (MovimJingles.sessions[jid] == undefined) { |
|||
throw Error('Proceed from a non initiated session ' + jid); |
|||
} |
|||
|
|||
MovimJingles.sessions[jid].sessionInitiate(fullJid, id, mujiRoom); |
|||
}, |
|||
|
|||
onMute: function (jid, name) { |
|||
if (MovimJingles.sessions[jid] == undefined) { |
|||
throw Error('Mute from a non initiated session ' + jid); |
|||
} |
|||
|
|||
MovimJingles.sessions[jid].onMute(name); |
|||
}, |
|||
|
|||
onUnmute: function (jid, name) { |
|||
if (MovimJingles.sessions[jid] == undefined) { |
|||
throw Error('Unmute from a non initiated session ' + jid); |
|||
} |
|||
|
|||
MovimJingles.sessions[jid].onUnmute(name); |
|||
}, |
|||
|
|||
onContentAdd: function (jid, sdp) { |
|||
if (MovimJingles.sessions[jid] == undefined) { |
|||
throw Error('Content add from a non initiated session ' + jid); |
|||
} |
|||
|
|||
MovimJingles.sessions[jid].onContentAdd(sdp); |
|||
}, |
|||
|
|||
onReplaceTrack: function (stream) { |
|||
let videoTrack = stream.getVideoTracks()[0]; |
|||
|
|||
for (jid of Object.keys(MovimJingles.sessions)) { |
|||
MovimJingles.sessions[jid].onReplaceTrack(videoTrack); |
|||
} |
|||
}, |
|||
|
|||
terminate: function (jid, reason) { |
|||
MovimJingles.sessions[jid].terminate(reason); |
|||
delete MovimJingles.sessions[jid]; |
|||
}, |
|||
|
|||
onTerminate: function (jid) { |
|||
if (MovimJingles.sessions[jid] == undefined) return; |
|||
|
|||
MovimJingles.sessions[jid].close(); |
|||
delete MovimJingles.sessions[jid]; |
|||
|
|||
let visio = document.querySelector('#visio'); |
|||
|
|||
// No sessions left
|
|||
if (Object.keys(MovimJingles.sessions).length == 0) { |
|||
if (visio.dataset.muji == 'false') { |
|||
MovimVisio.clear(); |
|||
} else { |
|||
let state = document.querySelector('p.state'); |
|||
state.innerText = MovimVisio.states.no_participants_left; |
|||
} |
|||
|
|||
} |
|||
}, |
|||
|
|||
terminateAll: function (reason) { |
|||
for (jid of Object.keys(MovimJingles.sessions)) { |
|||
MovimJingles.terminate(jid, reason); |
|||
} |
|||
|
|||
MovimVisio.clear(); |
|||
} |
|||
} |
|||
@ -0,0 +1,76 @@ |
|||
<?php |
|||
|
|||
namespace Moxl\Stanza; |
|||
|
|||
class JingleCallInvite |
|||
{ |
|||
public static function invite(string $to, string $id, string $room, bool $video = false) |
|||
{ |
|||
$dom = Message::factory($to, 'groupchat'); |
|||
|
|||
$invite = $dom->createElementNS('urn:xmpp:call-invites:0', 'invite'); |
|||
$invite->setAttribute('id', $id); |
|||
|
|||
if ($video) { |
|||
$invite->setAttribute('video', 'true'); |
|||
} |
|||
|
|||
$muji = $dom->createElement('muji'); |
|||
$muji->setAttribute('xmlns', 'urn:xmpp:jingle:muji:0'); |
|||
$muji->setAttribute('room', $room); |
|||
|
|||
$invite->appendChild($muji); |
|||
|
|||
$dom->documentElement->appendChild($invite); |
|||
|
|||
\Moxl\API::sendDom($dom); |
|||
} |
|||
|
|||
public static function retract(string $to, string $id) |
|||
{ |
|||
$dom = Message::factory($to, 'groupchat'); |
|||
|
|||
$retract = $dom->createElementNS('urn:xmpp:call-invites:0', 'retract'); |
|||
$retract->setAttribute('id', $id); |
|||
|
|||
$dom->documentElement->appendChild($retract); |
|||
|
|||
\Moxl\API::sendDom($dom); |
|||
} |
|||
|
|||
public static function accept(string $to, string $id) |
|||
{ |
|||
$dom = Message::factory($to, 'groupchat'); |
|||
|
|||
$accept = $dom->createElementNS('urn:xmpp:call-invites:0', 'accept'); |
|||
$accept->setAttribute('id', $id); |
|||
|
|||
$dom->documentElement->appendChild($accept); |
|||
|
|||
\Moxl\API::sendDom($dom); |
|||
} |
|||
|
|||
public static function reject(string $to, string $id) |
|||
{ |
|||
$dom = Message::factory($to, 'groupchat'); |
|||
|
|||
$reject = $dom->createElementNS('urn:xmpp:call-invites:0', 'reject'); |
|||
$reject->setAttribute('id', $id); |
|||
|
|||
$dom->documentElement->appendChild($reject); |
|||
|
|||
\Moxl\API::sendDom($dom); |
|||
} |
|||
|
|||
public static function left(string $to, string $id) |
|||
{ |
|||
$dom = Message::factory($to, 'groupchat'); |
|||
|
|||
$left = $dom->createElementNS('urn:xmpp:call-invites:0', 'left'); |
|||
$left->setAttribute('id', $id); |
|||
|
|||
$dom->documentElement->appendChild($left); |
|||
|
|||
\Moxl\API::sendDom($dom); |
|||
} |
|||
} |
|||
@ -0,0 +1,18 @@ |
|||
<?php |
|||
|
|||
namespace Moxl\Xec\Action\JingleCallInvite; |
|||
|
|||
use Moxl\Stanza\JingleCallInvite; |
|||
use Moxl\Xec\Action; |
|||
|
|||
class Accept extends Action |
|||
{ |
|||
protected string $_to; |
|||
protected string $_id; |
|||
|
|||
public function request() |
|||
{ |
|||
$this->store(); |
|||
JingleCallInvite::accept($this->_to, $this->_id); |
|||
} |
|||
} |
|||
@ -0,0 +1,33 @@ |
|||
<?php |
|||
|
|||
namespace Moxl\Xec\Action\JingleCallInvite; |
|||
|
|||
use Moxl\Stanza\JingleCallInvite; |
|||
use Moxl\Xec\Action; |
|||
use Moxl\Xec\Payload\CallInvitePropose; |
|||
|
|||
class Invite extends Action |
|||
{ |
|||
protected string $_to; |
|||
protected string $_id; |
|||
protected ?string $_room = null; |
|||
protected bool $_video = false; |
|||
|
|||
public function request() |
|||
{ |
|||
$this->store(); |
|||
JingleCallInvite::invite($this->_to, $this->_id, $this->_room, $this->_video); |
|||
} |
|||
|
|||
public function handle(?\SimpleXMLElement $stanza = null, ?\SimpleXMLElement $parent = null) |
|||
{ |
|||
$propose = new CallInvitePropose; |
|||
$propose->handle($stanza->invite, $stanza); |
|||
} |
|||
|
|||
public function enableVideo() |
|||
{ |
|||
$this->_video = true; |
|||
return $this; |
|||
} |
|||
} |
|||
@ -0,0 +1,18 @@ |
|||
<?php |
|||
|
|||
namespace Moxl\Xec\Action\JingleCallInvite; |
|||
|
|||
use Moxl\Stanza\JingleCallInvite; |
|||
use Moxl\Xec\Action; |
|||
|
|||
class Left extends Action |
|||
{ |
|||
protected string $_to; |
|||
protected string $_id; |
|||
|
|||
public function request() |
|||
{ |
|||
$this->store(); |
|||
JingleCallInvite::left($this->_to, $this->_id); |
|||
} |
|||
} |
|||
@ -0,0 +1,18 @@ |
|||
<?php |
|||
|
|||
namespace Moxl\Xec\Action\JingleCallInvite; |
|||
|
|||
use Moxl\Stanza\JingleCallInvite; |
|||
use Moxl\Xec\Action; |
|||
|
|||
class Reject extends Action |
|||
{ |
|||
protected string $_to; |
|||
protected string $_id; |
|||
|
|||
public function request() |
|||
{ |
|||
$this->store(); |
|||
JingleCallInvite::reject($this->_to, $this->_id); |
|||
} |
|||
} |
|||
@ -0,0 +1,34 @@ |
|||
<?php |
|||
|
|||
namespace Moxl\Xec\Action\JingleCallInvite; |
|||
|
|||
use App\Message; |
|||
use Moxl\Stanza\JingleCallInvite; |
|||
use Moxl\Xec\Action; |
|||
use SimpleXMLElement; |
|||
|
|||
class Retract extends Action |
|||
{ |
|||
protected string $_to; |
|||
protected string $_id; |
|||
|
|||
public function request() |
|||
{ |
|||
$this->store(); |
|||
JingleCallInvite::retract($this->_to, $this->_id); |
|||
} |
|||
|
|||
public function handle(?SimpleXMLElement $stanza = null, ?SimpleXMLElement $parent = null) |
|||
{ |
|||
$message = Message::eventMessageFactory( |
|||
'jingle', |
|||
baseJid($this->_to), |
|||
$this->_id |
|||
); |
|||
$message->type = 'muji_retract'; |
|||
$message->save(); |
|||
|
|||
$this->pack($message); |
|||
$this->event('muji_message'); |
|||
} |
|||
} |
|||
@ -0,0 +1,22 @@ |
|||
<?php |
|||
|
|||
namespace Moxl\Xec\Action\Muc; |
|||
|
|||
use Moxl\Xec\Action; |
|||
use Moxl\Stanza\Muc; |
|||
|
|||
class CreateMujiRoom extends Action |
|||
{ |
|||
protected $_to; |
|||
|
|||
public function request() |
|||
{ |
|||
$this->store(); |
|||
Muc::createMujiChat($this->_to); |
|||
} |
|||
|
|||
public function handle(?\SimpleXMLElement $stanza = null, ?\SimpleXMLElement $parent = null) |
|||
{ |
|||
$this->deliver(); |
|||
} |
|||
} |
|||
@ -0,0 +1,25 @@ |
|||
<?php |
|||
|
|||
namespace Moxl\Xec\Payload; |
|||
|
|||
use App\MujiCallParticipant; |
|||
|
|||
class CallInviteAccept extends Payload |
|||
{ |
|||
public function handle(?\SimpleXMLElement $stanza = null, ?\SimpleXMLElement $parent = null) |
|||
{ |
|||
if ($parent->{'stanza-id'} && $parent->{'stanza-id'}->attributes()->xmlns == 'urn:xmpp:sid:0') { |
|||
$muji = \App\User::me()->session->mujiCalls()->where('id', (string)$stanza->attributes()->id)->first(); |
|||
|
|||
if ($muji) { |
|||
MujiCallParticipant::firstOrCreate([ |
|||
'muji_call_id' => (string)$stanza->attributes()->id, |
|||
'jid' => (string)$parent->attributes()->from, |
|||
]); |
|||
|
|||
$this->pack($muji); |
|||
$this->deliver(); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,27 @@ |
|||
<?php |
|||
|
|||
namespace Moxl\Xec\Payload; |
|||
|
|||
use App\MujiCallParticipant; |
|||
use Carbon\Carbon; |
|||
|
|||
class CallInviteLeft extends Payload |
|||
{ |
|||
public function handle(?\SimpleXMLElement $stanza = null, ?\SimpleXMLElement $parent = null) |
|||
{ |
|||
if ($parent->{'stanza-id'} && $parent->{'stanza-id'}->attributes()->xmlns == 'urn:xmpp:sid:0') { |
|||
$muji = \App\User::me()->session->mujiCalls()->where('id', (string)$stanza->attributes()->id)->first(); |
|||
|
|||
if ($muji) { |
|||
MujiCallParticipant::firstOrCreate([ |
|||
'muji_call_id' => (string)$stanza->attributes()->id, |
|||
'jid' => (string)$parent->attributes()->from, |
|||
'left_at' => Carbon::now(), |
|||
]); |
|||
|
|||
$this->pack($muji); |
|||
$this->deliver(); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,64 @@ |
|||
<?php |
|||
|
|||
namespace Moxl\Xec\Payload; |
|||
|
|||
use App\Message; |
|||
use App\MujiCallParticipant; |
|||
use Movim\CurrentCall; |
|||
use Moxl\Xec\Action\JingleCallInvite\Reject; |
|||
|
|||
class CallInvitePropose extends Payload |
|||
{ |
|||
public function handle(?\SimpleXMLElement $stanza = null, ?\SimpleXMLElement $parent = null, bool $carbon = false) |
|||
{ |
|||
// Another session is already started
|
|||
if (CurrentCall::getInstance()->isStarted() |
|||
&& CurrentCall::getInstance()->isJidInCall(baseJid($parent->attributes()->from))) { |
|||
$conference = \App\User::me()->session->conferences()->where('conference', \baseJid((string)$parent->attributes()->from))->first(); |
|||
|
|||
if ($conference) { |
|||
// If the propose is from another person
|
|||
if (!$conference->presence || $conference->presence->resource != \explodeJid((string)$parent->attributes()->from)['resource']) { |
|||
$reject = new Reject; |
|||
$reject->setTo(\baseJid((string)$parent->attributes()->from)) |
|||
->setId((string)$stanza->attributes()->id) |
|||
->request(); |
|||
|
|||
return; |
|||
} |
|||
} |
|||
} |
|||
|
|||
if ($stanza->muji && $stanza->muji->attributes()->xmlns == 'urn:xmpp:jingle:muji:0' |
|||
&& $parent->{'stanza-id'} && $parent->{'stanza-id'}->attributes()->xmlns == 'urn:xmpp:sid:0') { |
|||
$muji = \App\MujiCall::firstOrCreate([ |
|||
'id' => (string)$stanza->attributes()->id, |
|||
'muc' => (string)$stanza->muji->attributes()->room, |
|||
'jidfrom' => $carbon |
|||
? (string)$parent->attributes()->to |
|||
: (string)$parent->{'stanza-id'}->attributes()->by, |
|||
'isfromconference' => ((string)$parent->attributes()->type == 'groupchat'), |
|||
'video' => ((string)$stanza->attributes()->video == 'true'), |
|||
]); |
|||
|
|||
MujiCallParticipant::firstOrCreate([ |
|||
'muji_call_id' => (string)$stanza->attributes()->id, |
|||
'jid' => (string)$parent->attributes()->from, |
|||
'inviter' => true |
|||
]); |
|||
|
|||
$message = Message::eventMessageFactory( |
|||
'muji_propose', |
|||
baseJid((string)$parent->attributes()->from), |
|||
(string)$stanza->attributes()->id |
|||
); |
|||
$message->save(); |
|||
|
|||
$this->pack($message); |
|||
$this->event('muji_message'); |
|||
|
|||
$this->pack($muji); |
|||
$this->deliver(); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,39 @@ |
|||
<?php |
|||
|
|||
namespace Moxl\Xec\Payload; |
|||
|
|||
use App\Message; |
|||
use App\MujiCall; |
|||
use Movim\CurrentCall; |
|||
|
|||
class CallInviteRetract extends Payload |
|||
{ |
|||
public function handle(?\SimpleXMLElement $stanza = null, ?\SimpleXMLElement $parent = null) |
|||
{ |
|||
if ($parent->{'stanza-id'} && $parent->{'stanza-id'}->attributes()->xmlns == 'urn:xmpp:sid:0') { |
|||
$muji = \App\User::me()->session->mujiCalls()->where('id', (string)$stanza->attributes()->id)->first(); |
|||
|
|||
if ($muji) { |
|||
$participant = $muji->participants->firstWhere('jid', $parent->attributes()->from); |
|||
|
|||
if ($participant && $participant->inviter) { |
|||
$message = Message::eventMessageFactory( |
|||
'muji_retract', |
|||
baseJid((string)$parent->attributes()->from), |
|||
(string)$stanza->attributes()->id |
|||
); |
|||
$message->save(); |
|||
|
|||
$this->pack($message); |
|||
$this->event('muji_message'); |
|||
|
|||
CurrentCall::getInstance()->stop(baseJid((string)$parent->attributes()->from), $muji->id); |
|||
MujiCall::where('id', $muji->id)->where('session_id', $muji->session_id)->delete(); |
|||
|
|||
$this->pack($muji); |
|||
$this->deliver(); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
Write
Preview
Loading…
Cancel
Save
Reference in new issue