import { isContentOutOfRange , ISharedContent } from '@models/ISharedContent' ; import { PARTICIPANT_SIZE } from '@models/Participant' ; import { diffMap } from '@models/utils' ; import { participantsStore as participants } from '@stores/participants' ; import { LocalParticipant } from '@stores/participants/LocalParticipant' ; import { RemoteParticipant } from '@stores/participants/RemoteParticipant' ; import contents from '@stores/sharedContents/SharedContents' ; import { JitsiRemoteTrack , JitsiTrack } from 'lib-jitsi-meet' ; import { autorun , IReactionDisposer , makeObservable , observable } from 'mobx' ; import { RemoteTrackInfo , TrackInfo } from './priorityTypes' ; export const PRIORITYLOG = false ; export const priorityLog = PRIORITYLOG ? console . log : ( a ) => { } ; export const priorityDebug = PRIORITYLOG ? console . debug : ( a ) => { } ; function extractParticipantTrackInfo ( participant , track ) { return { endpointId : participant . id , onStage : track . isAudioTrack ( ) && ( participant . physics . onStage || participants . yarnPhones . has ( participant . id ) || participant . inLocalsZone ) , pose : { ... participant . pose , } , size : [ 0 , 0 ] , offset : 0 , priority : 0 , muted : track . isAudioTrack ( ) ? participant . muteAudio : participant . muteVideo } ; } function extractContentTrackInfo ( content , track ) { return { endpointId : ( track as JitsiRemoteTrack ) . getParticipantId ( ) , onStage : false , pose : { ... content . pose , } , size : content . size , offset : - PARTICIPANT_SIZE * 10 , priority : 0 , muted : false , } ; } function extractMainTrackInfo ( mainTrack ) { return { endpointId : mainTrack . getParticipantId ( ) , onStage : true , pose : { position : [ 0 , 0 ] , orientation : 0 } , size : [ 0 , 0 ] , offset : - PARTICIPANT_SIZE * 100 , priority : 0 , muted : false , } ; } function extractPropsFromLocalParticipant ( participant ) { return { pose : { ... participant . pose , } , onStage : false , } ; } export class PriorityCalculator { private local : TrackInfo ; private updateAll = true ; private readonly updateSet = new Set < string > ( ) ; private limits = [ - 1 , - 1 ] ; private limitUpdated = false ; private readonly priorityMapForContent = new Map < string , RemoteTrackInfo > ( ) ; private readonly priorityMaps = [ new Map < string , RemoteTrackInfo > ( ) , new Map < string , RemoteTrackInfo > ( ) ] ; lastContentVideos : string [ ] = [ ] ; lastParticipantVideos : string [ ] = [ ] ; lastAudios : string [ ] = [ ] ; @ observable tracksToAccept : [ RemoteTrackInfo [ ] , RemoteTrackInfo [ ] ] = [ [ ] , [ ] ] ; private disposers : IReactionDisposer [ ] = [ ] ; private _enabled = false ; constructor ( ) { this . local = extractPropsFromLocalParticipant ( participants . local ) ; makeObservable ( this ) ; } clear ( ) { this . disable ( ) ; this . updateAll = true ; this . updateSet . clear ( ) ; this . priorityMaps . forEach ( m => m . clear ( ) ) ; this . priorityMapForContent . clear ( ) ; this . tracksToAccept = [ [ ] , [ ] ] ; this . lastContentVideos = [ ] ; this . lastParticipantVideos = [ ] ; this . lastAudios = [ ] ; } setLimits ( limits ) { this . limits [ 0 ] = limits [ 0 ] ; this . limits [ 1 ] = limits [ 1 ] ; this . limitUpdated = true ; } onRemoteTrackAdded ( track ) { this . updateSet . add ( track . getParticipantId ( ) ) ; } get enabled ( ) : boolean { return this . _enabled ; } enable ( ) { this . updateAll = true ; const localChangeDisposer = autorun ( ( ) => { const local = participants . local ; this . local = extractPropsFromLocalParticipant ( local ) ; this . updateAll = true ; } ) ; let oldRemoteParticipants = new Map < string , RemoteParticipant > ( ) ; const remoteChangeDisposer = autorun ( ( ) => { const newRemoteParticipants = new Map < string , RemoteParticipant > ( participants . remote ) ; const added = diffMap ( newRemoteParticipants , oldRemoteParticipants ) ; const removed = diffMap ( oldRemoteParticipants , newRemoteParticipants ) ; removed . forEach ( onRemoveParticipant ) ; added . forEach ( onAddParticipant ) ; oldRemoteParticipants = newRemoteParticipants ; } ) ; let oldRemoteContents = new Map < string , Set < JitsiRemoteTrack > > ( ) ; const remoteContentsChangeDisposer = autorun ( ( ) => { const newRemoteContents = new Map < string , Set < JitsiRemoteTrack > > ( contents . tracks . remoteContents ) ; const added = diffMap ( newRemoteContents , oldRemoteContents ) ; const removed = diffMap ( oldRemoteContents , newRemoteContents ) ; removed . forEach ( onRemoveContent ) ; added . forEach ( onAddContent ) ; oldRemoteContents = newRemoteContents ; } ) ; const remoteDiposers = new Map < string , IReactionDisposer > ( ) ; const onRemoveParticipant = ( rp ) => { const disposer = remoteDiposers . get ( rp . id ) ; if ( disposer === undefined ) { throw new Error ( `Cannot find disposer for remote participant with id: ${ rp . id }` ) ; } disposer ( ) ; this . priorityMaps . forEach ( priorityMap => priorityMap . delete ( rp . id ) ) ; remoteDiposers . delete ( rp . id ) ; this . updateSet . add ( rp . id ) ; priorityLog ( 'onRemoveParticipant:' , rp , this . priorityMaps [ 0 ] ) ; } ; const onRemoveContent = ( tracks , id ) => { const disposer = remoteDiposers . get ( id ) ; if ( disposer === undefined ) { throw new Error ( `Cannot find disposer for remote participant with id: ${ id }` ) ; } disposer ( ) ; this . priorityMapForContent . delete ( id ) ; this . priorityMaps [ 1 ] . delete ( id ) ; remoteDiposers . delete ( id ) ; this . updateSet . add ( id ) ; priorityLog ( 'onRemoveContent:' , id , this . priorityMapForContent ) ; } ; const onAddParticipant = ( rp ) => { remoteDiposers . set ( rp . id , autorun ( ( ) => { if ( rp . tracks . audio || rp . tracks . avatar ) { const important = rp . physics . onStage || participants . yarnPhones . has ( rp . id ) ; const moved = rp . pose . position ; this . updateSet . add ( rp . id ) ; } } ) ) ; priorityLog ( 'onAddParticipant:' , rp , this . priorityMaps [ 0 ] ) ; } ; const onAddContent = ( tracks , id ) => { remoteDiposers . set ( id , autorun ( ( ) => { if ( tracks . size ) { const content = contents . find ( id ) ; this . updateSet . add ( ( tracks . values ( ) . next ( ) . value as JitsiRemoteTrack ) . getParticipantId ( ) ) ; } } ) ) ; priorityLog ( 'onAddContent:' , id , this . priorityMapForContent ) ; } ; this . disposers = [ localChangeDisposer , remoteChangeDisposer , remoteContentsChangeDisposer ] ; this . _enabled = true ; } disable ( ) { this . disposers . forEach ( disposer => disposer ( ) ) ; this . _enabled = false ; } update ( ) { if ( ! this . haveUpdates ) { return ; } this . calcPriority ( ) ; this . updateAll = false ; this . updateSet . clear ( ) ; this . limitUpdated = false ; } private calcPriority ( ) { const remotes = Array . from ( participants . remote . values ( ) ) ; const recalculates = this . updateAll ? remotes : remotes . filter ( r => this . updateSet . has ( r . id ) ) ; for ( const rp of recalculates ) { [ rp . tracks . avatar , rp . tracks . audio ] . forEach ( ( track , idx ) => { if ( track ) { const trackInfo = extractParticipantTrackInfo ( rp , track ) ; trackInfo . priority = this . calcPriorityValue ( this . local , trackInfo ) ; this . priorityMaps [ idx ] . set ( rp . id , trackInfo ) ; } } ) ; } const contentTracks = Array . from ( contents . tracks . remoteContents . values ( ) ) ; const carrierIds = contentTracks . map ( ( tracks ) => { const next = tracks . values ( ) . next ( ) ; return next . done ? undefined : next . value . getParticipantId ( ) ; } ) ; const recalculateCarrierIds = carrierIds . filter ( pid => pid && ( this . updateAll ? true : this . updateSet . has ( pid ) ) ) ; recalculateCarrierIds . forEach ( ( pid ) => { const cid = contents . tracks . carrierMap . get ( pid ! ) ; const content = cid ? contents . find ( cid ) : undefined ; if ( content && ! isContentOutOfRange ( content ) ) { const tracks = Array . from ( contents . tracks . remoteContents . get ( content . id ) ! ) ; const videoAudio = [ tracks . find ( track => track . isVideoTrack ( ) ) , tracks . find ( track => track . isAudioTrack ( ) ) ] ; const trackInfos = [ ] ; videoAudio . forEach ( ( track , idx ) => { if ( track ) { trackInfos [ idx ] = extractContentTrackInfo ( content , track ) ; trackInfos [ idx ] . priority = this . calcPriorityValue ( this . local , trackInfos [ idx ] ) ; } } ) ; if ( trackInfos [ 0 ] ) { this . priorityMapForContent . set ( content . id , trackInfos [ 0 ] ) ; } if ( trackInfos [ 1 ] ) { this . priorityMaps [ 1 ] . set ( content . id , trackInfos [ 1 ] ) ; } } } ) ; const prioritizedContentTrackInfoList = Array . from ( this . priorityMapForContent . values ( ) ) . sort ( ( a , b ) => a . priority - b . priority ) ; const prioritizedTrackInfoLists = this . priorityMaps . map ( priorityMap => Array . from ( priorityMap . values ( ) ) . sort ( ( a , b ) => a . priority - b . priority ) ) as [ RemoteTrackInfo [ ] , RemoteTrackInfo [ ] ] ; const mainTracks = contents . tracks . remoteMainTrack ( ) ; if ( mainTracks ) { if ( mainTracks [ 0 ] ) { const mainVideoInfo = extractMainTrackInfo ( mainTracks [ 0 ] ) ; prioritizedContentTrackInfoList . unshift ( mainVideoInfo ) ; } if ( mainTracks [ 1 ] ) { const mainAudioInfo = extractMainTrackInfo ( mainTracks [ 1 ] ) ; prioritizedTrackInfoLists [ 1 ] . unshift ( mainAudioInfo ) ; } } const contentVideos = prioritizedContentTrackInfoList . filter ( info => ! info . muted ) ; const participantVideos = prioritizedTrackInfoLists [ 0 ] . filter ( info => ! info . muted ) ; if ( contentVideos . length > this . limits [ 0 ] ) { contentVideos . splice ( this . limits [ 0 ] ) ; participantVideos . splice ( 0 ) ; } else { participantVideos . splice ( this . limits [ 0 ] - contentVideos . length ) ; } const audios = prioritizedTrackInfoLists [ 1 ] ; const nMuted = prioritizedTrackInfoLists [ 1 ] . filter ( info => info . muted ) . length ; audios . splice ( Math . min ( this . limits [ 0 ] + nMuted , audios . length ) ) ; this . tracksToAccept = [ [ ... contentVideos , ... participantVideos ] , audios ] ; this . lastContentVideos = contentVideos . map ( info => info . endpointId ) ; this . lastParticipantVideos = participantVideos . map ( info => info . endpointId ) ; this . lastAudios = audios . map ( info => info . endpointId ) ; } private calcPriorityValue ( local , remote ) { if ( remote . onStage ) { return 0 ; } const delta = remote . size . map ( ( sz , idx ) => { let diff = remote . pose . position [ idx ] - local . pose . position [ idx ] ; if ( diff < 0 ) { diff += sz ; if ( diff > 0 ) { diff = 0 ; } } return diff ; } ) ; const distance = Math . sqrt ( delta [ 0 ] * delta [ 0 ] + delta [ 1 ] * delta [ 1 ] ) + remote . offset ; return distance ; } private get haveUpdates ( ) : boolean { return this . updateAll || this . updateSet . size !== 0 || this . limitUpdated ; } }