Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Autoplay and Media Component Improvements #7714

Merged
merged 4 commits into from
Mar 11, 2023
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/client-core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@
"devDependencies": {
"@emotion/react": "11.9.0",
"@emotion/styled": "11.8.1",
"@types/hark": "1.2.2",
"@types/node": "18.11.18",
"@types/react": "18.0.19",
"@types/react-router-dom": "5.3.3",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,13 +50,9 @@ export const LocationInstanceState = defineState({
})

export function useWorldInstance() {
const [state, setState] = React.useState(null as null | State<InstanceState>)
const worldInstanceState = useState(getMutableState(LocationInstanceState).instances)
const worldHostId = useState(getMutableState(NetworkState).hostIds.world)
useEffect(() => {
setState(worldHostId.value ? worldInstanceState[worldHostId.value] : null)
}, [worldInstanceState, worldHostId])
return state
return worldHostId.value ? worldInstanceState[worldHostId.value] : null
}

export const LocationInstanceConnectionServiceReceptor = (action) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,13 +50,9 @@ export const MediaInstanceState = defineState({
})

export function useMediaInstance() {
const [state, setState] = React.useState(null as null | State<InstanceState>)
const mediaInstanceState = useState(getMutableState(MediaInstanceState).instances)
const mediaHostId = useState(getMutableState(NetworkState).hostIds.media)
useEffect(() => {
setState(mediaHostId.value ? mediaInstanceState[mediaHostId.value] : null)
}, [mediaInstanceState, mediaHostId])
return state
return mediaHostId.value ? mediaInstanceState[mediaHostId.value] : null
}

export const MediaInstanceConnectionServiceReceptor = (action) => {
Expand Down
17 changes: 10 additions & 7 deletions packages/client-core/src/components/UserMediaWindow/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ export const useUserMediaWindowHook = ({ peerID, type }: Props) => {
const [videoTrackId, setVideoTrackId] = useState('')
const [audioTrackId, setAudioTrackId] = useState('')

const [harkListener, setHarkListener] = useState(null)
const [harkListener, setHarkListener] = useState(null as ReturnType<typeof hark> | null)
const [soundIndicatorOn, setSoundIndicatorOn] = useState(false)
const [isPiP, setPiP] = useState(false)
const [videoDisplayReady, setVideoDisplayReady] = useState<boolean>(false)
Expand All @@ -90,7 +90,6 @@ export const useUserMediaWindowHook = ({ peerID, type }: Props) => {

const [_volume, _setVolume] = useState(1)

const userHasInteracted = useEngineState().userHasInteracted
const selfUser = useAuthState().user.value
const currentLocation = useLocationState().currentLocation.location
const enableGlobalMute =
Expand Down Expand Up @@ -122,12 +121,16 @@ export const useUserMediaWindowHook = ({ peerID, type }: Props) => {
}, [peerMediaChannelState.audioStream])

useEffect(() => {
if (userHasInteracted.value && !isSelf) {
function onUserInteraction() {
videoElement?.play()
audioElement?.play()
if (harkListener) (harkListener as any).resume()
harkListener?.resume()
}
}, [userHasInteracted])
window.addEventListener('pointerdown', onUserInteraction)
return () => {
window.removeEventListener('pointerdown', onUserInteraction)
}
}, [videoElement, audioElement, harkListener])

useEffect(() => {
if (audioElement != null) {
Expand Down Expand Up @@ -160,7 +163,7 @@ export const useUserMediaWindowHook = ({ peerID, type }: Props) => {
audioTrackClones.forEach((track) => track.stop())
if (harkListener) (harkListener as any).stop()
}
}, [audioTrackId])
}, [audioTrackId, harkListener])

useEffect(() => {
videoElement.id = `${peerID}_video`
Expand Down Expand Up @@ -432,7 +435,7 @@ const UserMediaWindow = ({ peerID, type }: Props): JSX.Element => {
className={classNames({
[styles['video-wrapper']]: !isScreen,
[styles['screen-video-wrapper']]: isScreen,
[styles['border-lit']]: soundIndicatorOn
[styles['border-lit']]: soundIndicatorOn && !audioStreamPaused
})}
>
{(videoStream == null ||
Expand Down
23 changes: 11 additions & 12 deletions packages/engine/src/audio/systems/MediaSystem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,13 @@ import {
export class AudioEffectPlayer {
static instance = new AudioEffectPlayer()

constructor() {
// only init when running in client
if (isClient) {
this.#init()
}
}

static SOUNDS = {
notification: '/sfx/notification.mp3',
message: '/sfx/message.mp3',
Expand All @@ -56,7 +63,7 @@ export class AudioEffectPlayer {
// pool of elements
#els: HTMLAudioElement[] = []

_init() {
#init() {
if (this.#els.length) return
for (let i = 0; i < 20; i++) {
const audioElement = document.createElement('audio')
Expand Down Expand Up @@ -117,7 +124,6 @@ export default async function MediaSystem() {

const enableAudioContext = () => {
if (audioContext.state === 'suspended') audioContext.resume()
AudioEffectPlayer.instance._init()
}

if (isClient && !getMutableState(EngineState).isEditor.value) {
Expand All @@ -126,13 +132,10 @@ export default async function MediaSystem() {
const mediaQuery = defineQuery([MediaComponent, MediaElementComponent])
function handleAutoplay() {
enableAudioContext()

for (const entity of mediaQuery()) {
const media = getComponentState(entity, MediaComponent)
if (media.playing.value) return
if (media.paused.value && media.autoplay.value) media.paused.set(false)
// safari requires play() to be called in the DOM handle function
getComponent(entity, MediaElementComponent)?.element.play()
const mediaElement = getComponent(entity, MediaElementComponent)
const media = getComponent(entity, MediaComponent)
if (!media.paused && mediaElement?.element.paused) mediaElement.element.play()
}
}
// TODO: add destroy callbacks
Expand Down Expand Up @@ -230,8 +233,6 @@ export default async function MediaSystem() {
addActionReceptor(AudioSettingReceptor)
addActionReceptor(MediaSettingReceptor)

const userInteractActionQueue = createActionQueue(EngineActions.setUserHasInteracted.matches)

const mediaQuery = defineQuery([MediaComponent])
const videoQuery = defineQuery([VideoComponent])
const volumetricQuery = defineQuery([VolumetricComponent, MediaElementComponent])
Expand Down Expand Up @@ -273,8 +274,6 @@ export default async function MediaSystem() {
audioState.gainNodeMixBuses.soundEffects.value.disconnect()
audioState.gainNodeMixBuses.soundEffects.set(null!)

removeActionQueue(userInteractActionQueue)

removeQuery(mediaQuery)
removeQuery(videoQuery)
removeQuery(volumetricQuery)
Expand Down
2 changes: 0 additions & 2 deletions packages/engine/src/ecs/classes/EngineState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ export const EngineState = defineState({
isTeleporting: false,
leaveWorld: false,
socketInstance: false,
userHasInteracted: false,
spectating: false,
usersTyping: {} as { [key: string]: true },
avatarLoadingEffect: true,
Expand Down Expand Up @@ -58,7 +57,6 @@ export function EngineEventReceptor(a) {
.when(EngineActions.sceneLoadingProgress.matches, (action) => s.merge({ loadingProgress: action.progress }))
.when(EngineActions.connectToWorld.matches, (action) => s.merge({ connectedWorld: action.connectedWorld }))
.when(EngineActions.setTeleporting.matches, (action) => s.merge({ isTeleporting: action.isTeleporting }))
.when(EngineActions.setUserHasInteracted.matches, (action) => s.merge({ userHasInteracted: true }))
.when(EngineActions.spectateUser.matches, (action) => s.spectating.set(!!action.user))
}

Expand Down
51 changes: 21 additions & 30 deletions packages/engine/src/scene/components/MediaComponent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,25 +105,24 @@ export const MediaComponent = defineComponent({
return {
controls: false,
synchronize: true,
autoplay: true,
/**
* TODO: refactor this into a ScheduleComponent for invoking callbacks at scheduled times
* The auto start time for the playlist, in Unix/Epoch time (milliseconds).
* If this value is negative, media playback must be explicitly started.
* If this value is non-negative and in the past, playback as soon as possible.
* If this value is in the future, begin playback at the appointed time.
*/
// autoStartTime: -1,
paths: [] as string[],
paused: true,
volume: 1,
playMode: PlayMode.loop as PlayMode,
isMusic: false,
volume: 1,
// runtime props
paused: true,
playing: false,
waiting: false,
track: 0,
trackDurations: [] as number[],
helper: null as Mesh<PlaneGeometry, MeshBasicMaterial> | null
/**
* TODO: refactor this into a ScheduleComponent for invoking callbacks at scheduled times
* The auto start time for the playlist, in Unix/Epoch time (milliseconds).
* If this value is negative, media playback must be explicitly started.
* If this value is non-negative and in the past, playback as soon as possible.
* If this value is in the future, begin playback at the appointed time.
*/
// autoStartTime: -1
}
},

Expand All @@ -134,7 +133,7 @@ export const MediaComponent = defineComponent({
toJSON: (entity, component) => {
return {
controls: component.controls.value,
autoplay: component.autoplay.value,
paused: component.paused.value,
paths: component.paths.value,
volume: component.volume.value,
synchronize: component.synchronize.value,
Expand All @@ -155,9 +154,6 @@ export const MediaComponent = defineComponent({
if (typeof json.controls === 'boolean' && json.controls !== component.controls.value)
component.controls.set(json.controls)

if (typeof json.autoplay === 'boolean' && json.autoplay !== component.autoplay.value)
component.autoplay.set(json.autoplay)

// backwars-compat: convert from number enums to strings
if (
(typeof json.playMode === 'number' || typeof json.playMode === 'string') &&
Expand Down Expand Up @@ -185,6 +181,9 @@ export const MediaComponent = defineComponent({

if (typeof json.isMusic === 'boolean' && component.isMusic.value !== json.isMusic)
component.isMusic.set(json.isMusic)

// @ts-ignore deprecated autoplay field
if (json.autoplay) component.paused.set(false)
})

return component
Expand All @@ -201,7 +200,6 @@ export function MediaReactor({ root }: EntityReactorProps) {

const media = useComponent(entity, MediaComponent)
const mediaElement = useOptionalComponent(entity, MediaElementComponent)
const userHasInteracted = useHookstate(getMutableState(EngineState).userHasInteracted)
const audioContext = getState(AudioState).audioContext
const gainNodeMixBuses = getState(AudioState).gainNodeMixBuses

Expand Down Expand Up @@ -243,9 +241,6 @@ export function MediaReactor({ root }: EntityReactorProps) {
tempElement.src = path
}

// handle autoplay
if (media.autoplay.value && userHasInteracted.value) media.paused.set(false)

return () => {
for (const { tempElement, listener } of metadataListeners) {
tempElement.removeEventListener('loadedmetadata', listener)
Expand All @@ -257,13 +252,6 @@ export function MediaReactor({ root }: EntityReactorProps) {
[media.paths]
)

useEffect(
function updatePausedUponInteract() {
if (userHasInteracted.value && media.autoplay.value) media.paused.set(false)
},
[userHasInteracted]
)

useEffect(
function updateMediaElement() {
if (!isClient) return
Expand Down Expand Up @@ -302,16 +290,18 @@ export function MediaReactor({ root }: EntityReactorProps) {
element.addEventListener(
'playing',
() => {
media.playing.set(true), clearErrors(entity, MediaElementComponent)
media.waiting.set(false)
clearErrors(entity, MediaElementComponent)
},
{ signal }
)
element.addEventListener('waiting', () => media.playing.set(false), { signal })
element.addEventListener('waiting', () => media.waiting.set(true), { signal })
element.addEventListener(
'error',
(err) => {
addError(entity, MediaElementComponent, 'MEDIA_ERROR', err.message)
if (media.playing.value) media.track.set(getNextTrack(media.value))
if (!media.paused.value) media.track.set(getNextTrack(media.value))
media.waiting.set(false)
},
{ signal }
)
Expand All @@ -321,6 +311,7 @@ export function MediaReactor({ root }: EntityReactorProps) {
() => {
if (media.playMode.value === PlayMode.single) return
media.track.set(getNextTrack(media.value))
media.waiting.set(false)
},
{ signal }
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -125,8 +125,6 @@ export const updateVolumetric = async (entity: Entity) => {
} else {
volumetric.loadingEffectActive = false
endLoadingEffect(volumetric.entity, player.mesh)
const media = getComponentState(entity, MediaComponent)
if (media.autoplay.value && getMutableState(EngineState).userHasInteracted.value) media.paused.set(false)
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion packages/projects/default-project/apartment.scene.json
Original file line number Diff line number Diff line change
Expand Up @@ -897,7 +897,7 @@
"name": "media",
"props": {
"controls": false,
"autoplay": true,
"paused": false,
"paths": [
"__$project$__/default-project/assets/apartment-ambience.mp3"
],
Expand Down
6 changes: 3 additions & 3 deletions packages/projects/default-project/sky-station.scene.json
Original file line number Diff line number Diff line change
Expand Up @@ -314,7 +314,7 @@
"name": "media",
"props": {
"controls": true,
"autoplay": true,
"paused": false,
"paths": [
"__$project$__/default-project/assets/SampleVideo.mp4"
],
Expand Down Expand Up @@ -581,7 +581,7 @@
"name": "media",
"props": {
"controls": true,
"autoplay": false,
"paused": true,
"paths": [
"__$project$__/default-project/assets/SampleAudio.mp3"
],
Expand Down Expand Up @@ -785,7 +785,7 @@
"name": "media",
"props": {
"controls": false,
"autoplay": true,
"paused": false,
"paths": [
"__$project$__/default-project/assets/sky-station-ambience-loop.mp3"
],
Expand Down