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

fix: Use refs to select video tag to fix Safari bug #15

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all 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
279 changes: 222 additions & 57 deletions src/components/CallTile.vue
Original file line number Diff line number Diff line change
Expand Up @@ -26,16 +26,7 @@
</template>

<div v-if="participants" class="participants-container">
<template v-for="p in participants" :key="p.session_id">
<video-tile
:participant="p"
:handle-video-click="handleVideoClick"
:handle-audio-click="handleAudioClick"
:handle-screenshare-click="handleScreenshareClick"
:leave-call="leaveAndCleanUp"
:disable-screen-share="screen && !screen?.local"
/>
</template>
<div id="video-call" ref="videoCall"></div>

<template v-if="count === 1">
<waiting-card :url="roomUrl" />
Expand All @@ -51,33 +42,46 @@
</template>

<script>
import { defineComponent } from "vue";

import daily from "@daily-co/daily-js";

import WaitingCard from "./WaitingCard.vue";
import ChatTile from "./ChatTile.vue";
import VideoTile from "./VideoTile.vue";
import ScreenshareTile from "./ScreenshareTile.vue";
import LoadingTile from "./LoadingTile.vue";
import PermissionsErrorMsg from "./PermissionsErrorMsg.vue";

export default {
export default defineComponent({
name: "CallTile",
components: {
VideoTile,
WaitingCard,
ChatTile,
ScreenshareTile,
LoadingTile,
PermissionsErrorMsg,
},
props: ["leaveCall", "name", "roomUrl"],
props: {
leaveCall: {
type: Function,
required: true,
},
name: {
type: String,
required: true,
},
roomUrl: {
type: String,
required: true,
},
},
data() {
return {
callObject: null,
participants: null,
participants: [],
count: 0,
messages: [],
error: false,
error: '',
loading: false,
showPermissionsError: false,
screen: null,
Expand All @@ -98,63 +102,218 @@ export default {

// Add call and participant event handler
// Visit https://docs.daily.co/reference/daily-js/events for more event info
co.on("joining-meeting", this.handleJoiningMeeting)
.on("joined-meeting", this.updateParticpants)
.on("participant-joined", this.updateParticpants)
.on("participant-updated", this.updateParticpants)
.on("participant-left", this.updateParticpants)
co
.on("error", this.handleError)
// camera-error = device permissions issue
.on("camera-error", this.handleDeviceError)
// app-message handles receiving remote chat messages
.on("app-message", this.updateMessages);
.on("app-message", this.updateMessages)
.on("track-started", (p) => {
if (!p?.participant) return;
const tracks = this.getParticipantTracks(p.participant);
try {
this.updateMedia(p.participant.session_id, tracks);
} catch (e) {
console.warn(e);
}
})
.on("track-stopped", (p) => {
if (!p?.participant) return;
const tracks = this.getParticipantTracks(p.participant);
try {
this.updateMedia(p.participant.session_id, tracks);
} catch (e) {
console.warn(e);
}
});
},
unmounted() {
if (!this.callObject) return;
// Clean-up event handlers
this.callObject
.off("joining-meeting", this.handleJoiningMeeting)
.off("joined-meeting", this.updateParticpants)
.off("participant-joined", this.updateParticpants)
.off("participant-updated", this.updateParticpants)
.off("participant-left", this.updateParticpants)
.off("error", this.handleError)
.off("camera-error", this.handleDeviceError)
.off("app-message", this.updateMessages);
.off("app-message", this.updateMessages)
.off("track-started", (p) => {
if (!p?.participant) return;
const tracks = this.getParticipantTracks(p.participant);
try {
this.updateMedia(p.participant.session_id, tracks);
} catch (e) {
console.warn(e);
}
})
.off("track-stopped", (p) => {
if (!p?.participant) return;
const tracks = this.getParticipantTracks(p.participant);
try {
this.updateMedia(p.participant.session_id, tracks);
} catch (e) {
console.warn(e);
}
});
},
methods: {
/**
* This is called any time a participant update registers.
* In large calls, this should be optimized to avoid re-renders.
* For example, track-started and track-stopped can be used
* to register only video/audio/screen track changes.
*/
updateParticpants(e) {
console.log("[EVENT] ", e);
if (!this.callObject) return;
updateMedia(participantID, newTracks) {
// Get the video tag.
let videoTile = document.getElementById(
participantID
);
if (!videoTile) {
const videoCall = this.$refs.videoCall;

const newVideoTile = document.createElement("video");
newVideoTile.id = participantID;
videoCall.appendChild(newVideoTile);
videoTile = newVideoTile;
}

const p = this.callObject.participants();
this.count = Object.values(p).length;
this.participants = Object.values(p);
const video = videoTile;

const screen = this.participants.filter((p) => p.screenVideoTrack);
if (screen?.length && !this.screen) {
console.log("[SCREEN]", screen);
this.screen = screen[0];
} else if (!screen?.length && this.screen) {
this.screen = null;
// Get existing MediaStream from the video tag source object.
const existingStream = video.srcObject;

const newVideo = newTracks.videoTrack;
const newAudio = newTracks.audioTrack;

// If there is no existing stream or it contains no tracks,
// Just create a new media stream using our new tracks.
// This will happen if this is the first time we're
// setting the tracks.
if (!existingStream || existingStream.getTracks().length === 0) {
const tracks = [];
if (newVideo) tracks.push(newVideo);
if (newAudio) tracks.push(newAudio);
const newStream = new MediaStream(tracks);
video.srcObject = newStream;
video.playsInline = true;
video.autoplay = true;
video.muted = true;
this.playMedia(video);
return;
}

// This boolean will define whether we play the video element again
// This should be `true` if any of the tracks have changed.
let needsPlay = this.refreshAudioTrack(existingStream, newAudio);

// We have an extra if check here compared to the audio track
// handling above, because the video track also dictates
// whether we should hide the video DOM element.
if (newVideo) {
if (this.refreshVideoTrack(existingStream, newVideo) && !needsPlay) {
needsPlay = true;
}

video.classList.remove("hidden");
} else {
// If there's no video to be played, hide the element.
video.classList.add("hidden");
}
if (needsPlay) {
this.playMedia(video);
}
this.loading = false;
},
refreshAudioTrack(
existingStream,
newAudioTrack
) {
// If there is no new track, just early out
// and keep the old track on the stream as-is.
if (!newAudioTrack) return false;
const existingTracks = existingStream.getAudioTracks();
return this.refreshTrack(existingStream, existingTracks, newAudioTrack);
},
refreshVideoTrack(
existingStream,
newVideoTrack
) {
// If there is no new track, just early out
// and keep the old track on the stream as-is.
if (!newVideoTrack) return false;
const existingTracks = existingStream.getVideoTracks();
return this.refreshTrack(existingStream, existingTracks, newVideoTrack);
},
refreshTrack(
existingStream,
oldTracks,
newTrack
) {
const trackCount = oldTracks.length;
// If there is no matching old track,
// just add the new track.
if (trackCount === 0) {
existingStream.addTrack(newTrack);
return true;
}
if (trackCount > 1) {
console.warn(
`expected up to 1 media track, but got ${trackCount}. Only using the first one.`
);
}
const oldTrack = oldTracks[0];
// If the IDs of the old and new track don't match,
// replace the old track with the new one.
if (oldTrack.id !== newTrack.id) {
existingStream.removeTrack(oldTrack);
existingStream.addTrack(newTrack);
return true;
}
return false;
},
playMedia(video) {
const isPlaying =
!video.paused &&
!video.ended &&
video.currentTime > 0 &&
video.readyState > video.HAVE_CURRENT_DATA;

if (isPlaying) return;

video.play().catch((e) => {
if (e instanceof Error && e.name === "NotAllowedError") {
throw new Error("Autoplay error");
}

console.warn("Failed to play media.", e);
});
},
getParticipantTracks(p) {
const mediaTracks = {
videoTrack: null,
audioTrack: null,
};

const tracks = p?.tracks;
if (!tracks) return mediaTracks;

const videoTrack = tracks.video;
const videoState = videoTrack?.state;
if (videoTrack.persistentTrack && !(videoState === "off" || videoState === "blocked")) {
mediaTracks.videoTrack = videoTrack.persistentTrack;
}

// Only get audio track if this is a remote participant
if (!p.local) {
const audioTrack = tracks.audio;
const audioState = audioTrack?.state;
if (audioTrack.persistentTrack && !(audioState === "off" || audioState === "blocked")) {
mediaTracks.audioTrack = audioTrack.persistentTrack;
}
}
return mediaTracks;
},
// Add chat message to local message array
updateMessages(e) {
if (!e) return;
console.log("[MESSAGE] ", e.data);
this.messages.push(e?.data);
this.messages.push(e.data);
},
// Show local error in UI when daily-js reports an error
handleError(e) {
if (!e) return;
console.log("[ERROR] ", e);
this.error = e?.errorMsg;
this.error = e.errorMsg;
this.loading = false;
},
// Temporary show loading view while joining the call
Expand All @@ -163,13 +322,13 @@ export default {
},
// Toggle local microphone in use (on/off)
handleAudioClick() {
const audioOn = this.callObject.localAudio();
this.callObject.setLocalAudio(!audioOn);
const audioOn = this.callObject?.localAudio();
this.callObject?.setLocalAudio(!audioOn);
},
// Toggle local camera in use (on/off)
handleVideoClick() {
const videoOn = this.callObject.localVideo();
this.callObject.setLocalVideo(!videoOn);
const videoOn = this.callObject?.localVideo();
this.callObject?.setLocalVideo(!videoOn);
},
// Show permissions error in UI to alert local participant
handleDeviceError() {
Expand All @@ -178,10 +337,10 @@ export default {
// Toggle screen share
handleScreenshareClick() {
if (this.screen?.local) {
this.callObject.stopScreenShare();
this.callObject?.stopScreenShare();
this.screen = null;
} else {
this.callObject.startScreenShare();
this.callObject?.startScreenShare();
}
},
/**
Expand All @@ -191,6 +350,7 @@ export default {
* own messages.
*/
sendMessage(text) {
if (!this.callObject) return;
// Attach the local participant's username to the message to be displayed in ChatTile.vue
const local = this.callObject.participants().local;
const message = { message: text, name: local?.user_name || "Guest" };
Expand All @@ -199,19 +359,20 @@ export default {
},
// leave call, destroy call object, and reset local state values
leaveAndCleanUp() {
if (!this.callObject) return;
if (this.screen?.local) {
this.callObject.stopScreenShare();
}
this.callObject.leave().then(() => {
this.callObject.destroy();
this.callObject?.destroy();
jamsea marked this conversation as resolved.
Show resolved Hide resolved

this.participantWithScreenshare = null;
this.screen = null;
this.leaveCall();
});
},
},
};
});
</script>

<style scoped>
Expand Down Expand Up @@ -269,4 +430,8 @@ p {
padding: 8px 12px;
cursor: pointer;
}

#video-call {
min-width: 50%;
}
</style>
Loading