Add timer, comments, and fix camera switch for live
- Add elapsed time counter (MM:SS) in header and stats - Add comments section with real-time input - Fix camera switch to work during live (replaceVideoTrack) - Send comments to backend on finalize for saving - Show comment count in end stream dialog 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
540f213014
commit
870921a482
|
|
@ -551,6 +551,20 @@ body[data-theme="light"] .sw-video-modal-subtitle {
|
||||||
box-shadow: 0 0 15px rgba(239, 68, 68, 0.5);
|
box-shadow: 0 0 15px rgba(239, 68, 68, 0.5);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.sw-broadcast-timer {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.35rem;
|
||||||
|
padding: 0.3rem 0.7rem;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: rgba(56, 189, 248, 0.2);
|
||||||
|
border: 1px solid rgba(56, 189, 248, 0.4);
|
||||||
|
color: #93c5fd;
|
||||||
|
font-size: 0.72rem;
|
||||||
|
font-weight: 700;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
}
|
||||||
|
|
||||||
.sw-broadcast-dot {
|
.sw-broadcast-dot {
|
||||||
width: 8px;
|
width: 8px;
|
||||||
height: 8px;
|
height: 8px;
|
||||||
|
|
@ -1109,6 +1123,150 @@ body[data-theme="light"] .sw-video-modal-subtitle {
|
||||||
font-size: 0.85rem;
|
font-size: 0.85rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.sw-broadcast-confirm-comments {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 0.4rem;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: #93c5fd;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== COMMENTS SECTION ===== */
|
||||||
|
.sw-broadcast-comments {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-height: 0;
|
||||||
|
margin-top: 0.75rem;
|
||||||
|
border-top: 1px solid rgba(56, 189, 248, 0.15);
|
||||||
|
padding-top: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sw-broadcast-comments-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #94a3b8;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sw-broadcast-comments-header i {
|
||||||
|
color: #38bdf8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sw-broadcast-comments-list {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 80px;
|
||||||
|
max-height: 200px;
|
||||||
|
overflow-y: auto;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.35rem;
|
||||||
|
padding-right: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sw-broadcast-comments-list::-webkit-scrollbar {
|
||||||
|
width: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sw-broadcast-comments-list::-webkit-scrollbar-track {
|
||||||
|
background: rgba(15, 23, 42, 0.5);
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sw-broadcast-comments-list::-webkit-scrollbar-thumb {
|
||||||
|
background: rgba(56, 189, 248, 0.4);
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sw-broadcast-no-comments {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
height: 60px;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: #64748b;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sw-broadcast-comment {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.4rem;
|
||||||
|
padding: 0.4rem 0.5rem;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: rgba(15, 23, 42, 0.6);
|
||||||
|
border: 1px solid rgba(56, 189, 248, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sw-broadcast-comment-author {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #38bdf8;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sw-broadcast-comment-author::after {
|
||||||
|
content: ":";
|
||||||
|
}
|
||||||
|
|
||||||
|
.sw-broadcast-comment-text {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: #e5e7eb;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sw-broadcast-comment-input-row {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sw-broadcast-comment-input {
|
||||||
|
flex: 1;
|
||||||
|
padding: 0.5rem 0.7rem;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid rgba(56, 189, 248, 0.3);
|
||||||
|
background: rgba(15, 23, 42, 0.8);
|
||||||
|
color: #f8fafc;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sw-broadcast-comment-input:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: rgba(56, 189, 248, 0.6);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sw-broadcast-comment-input::placeholder {
|
||||||
|
color: #64748b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sw-broadcast-comment-send {
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: none;
|
||||||
|
background: linear-gradient(135deg, #38bdf8, #0ea5e9);
|
||||||
|
color: #fff;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sw-broadcast-comment-send:hover:not(:disabled) {
|
||||||
|
transform: scale(1.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sw-broadcast-comment-send:disabled {
|
||||||
|
opacity: 0.4;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
/* ===== VIEWER MODAL ===== */
|
/* ===== VIEWER MODAL ===== */
|
||||||
.sw-viewer-modal {
|
.sw-viewer-modal {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,16 @@ export default function LiveKitBroadcast({ coords, onClose, onStreamCreated }) {
|
||||||
const [livePostId, setLivePostId] = useState(null);
|
const [livePostId, setLivePostId] = useState(null);
|
||||||
const [facingMode, setFacingMode] = useState("user");
|
const [facingMode, setFacingMode] = useState("user");
|
||||||
const [isSwitchingCamera, setIsSwitchingCamera] = useState(false);
|
const [isSwitchingCamera, setIsSwitchingCamera] = useState(false);
|
||||||
|
|
||||||
|
// Timer state
|
||||||
|
const [liveStartTime, setLiveStartTime] = useState(null);
|
||||||
|
const [elapsedTime, setElapsedTime] = useState(0);
|
||||||
|
|
||||||
|
// Comments state
|
||||||
|
const [comments, setComments] = useState([]);
|
||||||
|
const [commentInput, setCommentInput] = useState("");
|
||||||
|
const commentsEndRef = useRef(null);
|
||||||
|
|
||||||
const [liveCoords] = useState(() => {
|
const [liveCoords] = useState(() => {
|
||||||
if (coords && typeof coords === "object") {
|
if (coords && typeof coords === "object") {
|
||||||
if (Array.isArray(coords) && coords.length >= 2) {
|
if (Array.isArray(coords) && coords.length >= 2) {
|
||||||
|
|
@ -44,7 +54,7 @@ export default function LiveKitBroadcast({ coords, onClose, onStreamCreated }) {
|
||||||
viewerCount,
|
viewerCount,
|
||||||
startBroadcast,
|
startBroadcast,
|
||||||
stopBroadcast,
|
stopBroadcast,
|
||||||
toggleAudio,
|
replaceVideoTrack,
|
||||||
} = useLiveKit({
|
} = useLiveKit({
|
||||||
roomName,
|
roomName,
|
||||||
userID,
|
userID,
|
||||||
|
|
@ -84,6 +94,29 @@ export default function LiveKitBroadcast({ coords, onClose, onStreamCreated }) {
|
||||||
}
|
}
|
||||||
}, [localStream]);
|
}, [localStream]);
|
||||||
|
|
||||||
|
// Timer effect - update every second when live
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isLive || !liveStartTime) return;
|
||||||
|
|
||||||
|
const interval = setInterval(() => {
|
||||||
|
setElapsedTime(Math.floor((Date.now() - liveStartTime) / 1000));
|
||||||
|
}, 1000);
|
||||||
|
|
||||||
|
return () => clearInterval(interval);
|
||||||
|
}, [isLive, liveStartTime]);
|
||||||
|
|
||||||
|
// Auto-scroll comments
|
||||||
|
useEffect(() => {
|
||||||
|
commentsEndRef.current?.scrollIntoView({ behavior: "smooth" });
|
||||||
|
}, [comments]);
|
||||||
|
|
||||||
|
// Format elapsed time as MM:SS
|
||||||
|
const formatTime = (seconds) => {
|
||||||
|
const mins = Math.floor(seconds / 60);
|
||||||
|
const secs = seconds % 60;
|
||||||
|
return `${mins.toString().padStart(2, "0")}:${secs.toString().padStart(2, "0")}`;
|
||||||
|
};
|
||||||
|
|
||||||
// Switch between front and back camera
|
// Switch between front and back camera
|
||||||
const switchCamera = useCallback(async () => {
|
const switchCamera = useCallback(async () => {
|
||||||
if (isSwitchingCamera) return;
|
if (isSwitchingCamera) return;
|
||||||
|
|
@ -98,9 +131,15 @@ export default function LiveKitBroadcast({ coords, onClose, onStreamCreated }) {
|
||||||
audio: true,
|
audio: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Stop old tracks
|
// Stop old video tracks only (keep audio reference for LiveKit)
|
||||||
if (localStream) {
|
if (localStream) {
|
||||||
localStream.getTracks().forEach((t) => t.stop());
|
localStream.getVideoTracks().forEach((t) => t.stop());
|
||||||
|
}
|
||||||
|
|
||||||
|
// If live, replace the video track in LiveKit
|
||||||
|
if (isLive) {
|
||||||
|
const newVideoTrack = newStream.getVideoTracks()[0];
|
||||||
|
await replaceVideoTrack(newVideoTrack);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update state
|
// Update state
|
||||||
|
|
@ -118,7 +157,22 @@ export default function LiveKitBroadcast({ coords, onClose, onStreamCreated }) {
|
||||||
} finally {
|
} finally {
|
||||||
setIsSwitchingCamera(false);
|
setIsSwitchingCamera(false);
|
||||||
}
|
}
|
||||||
}, [facingMode, localStream, isSwitchingCamera]);
|
}, [facingMode, localStream, isSwitchingCamera, isLive, replaceVideoTrack]);
|
||||||
|
|
||||||
|
// Add a comment
|
||||||
|
const handleAddComment = useCallback(() => {
|
||||||
|
if (!commentInput.trim()) return;
|
||||||
|
|
||||||
|
const newComment = {
|
||||||
|
id: Date.now(),
|
||||||
|
author: username || "Host",
|
||||||
|
text: commentInput.trim(),
|
||||||
|
timestamp: Date.now(),
|
||||||
|
};
|
||||||
|
|
||||||
|
setComments((prev) => [...prev, newComment]);
|
||||||
|
setCommentInput("");
|
||||||
|
}, [commentInput, username]);
|
||||||
|
|
||||||
const handleGoLive = useCallback(async () => {
|
const handleGoLive = useCallback(async () => {
|
||||||
if (!authToken) {
|
if (!authToken) {
|
||||||
|
|
@ -138,6 +192,10 @@ export default function LiveKitBroadcast({ coords, onClose, onStreamCreated }) {
|
||||||
const videoTrack = localStream.getVideoTracks()[0];
|
const videoTrack = localStream.getVideoTracks()[0];
|
||||||
await startBroadcast(videoTrack, true);
|
await startBroadcast(videoTrack, true);
|
||||||
|
|
||||||
|
// Start timer
|
||||||
|
setLiveStartTime(Date.now());
|
||||||
|
setElapsedTime(0);
|
||||||
|
|
||||||
if (onStreamCreated) {
|
if (onStreamCreated) {
|
||||||
onStreamCreated({
|
onStreamCreated({
|
||||||
roomName,
|
roomName,
|
||||||
|
|
@ -207,6 +265,7 @@ export default function LiveKitBroadcast({ coords, onClose, onStreamCreated }) {
|
||||||
|
|
||||||
if (livePostId) {
|
if (livePostId) {
|
||||||
try {
|
try {
|
||||||
|
// Include comments in finalize request
|
||||||
const res = await fetch(`${API_BASE}/api/live/finalize`, {
|
const res = await fetch(`${API_BASE}/api/live/finalize`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
|
|
@ -217,6 +276,11 @@ export default function LiveKitBroadcast({ coords, onClose, onStreamCreated }) {
|
||||||
post_id: livePostId,
|
post_id: livePostId,
|
||||||
save: !!save,
|
save: !!save,
|
||||||
video_key: videoKey,
|
video_key: videoKey,
|
||||||
|
comments: save ? comments.map((c) => ({
|
||||||
|
author: c.author,
|
||||||
|
text: c.text,
|
||||||
|
timestamp: c.timestamp,
|
||||||
|
})) : [],
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
if (res.ok && save) {
|
if (res.ok && save) {
|
||||||
|
|
@ -237,7 +301,7 @@ export default function LiveKitBroadcast({ coords, onClose, onStreamCreated }) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
onClose?.();
|
onClose?.();
|
||||||
}, [stopBroadcast, localStream, onClose, livePostId, authToken, roomName]);
|
}, [stopBroadcast, localStream, onClose, livePostId, authToken, roomName, comments]);
|
||||||
|
|
||||||
const handleBackdropClick = (e) => {
|
const handleBackdropClick = (e) => {
|
||||||
if (e.target === e.currentTarget && !isLive) {
|
if (e.target === e.currentTarget && !isLive) {
|
||||||
|
|
@ -257,10 +321,16 @@ export default function LiveKitBroadcast({ coords, onClose, onStreamCreated }) {
|
||||||
<i className="fa-solid fa-broadcast-tower" />
|
<i className="fa-solid fa-broadcast-tower" />
|
||||||
<span>{isLive ? "LIVE" : "Go Live"}</span>
|
<span>{isLive ? "LIVE" : "Go Live"}</span>
|
||||||
{isLive && (
|
{isLive && (
|
||||||
|
<>
|
||||||
<div className="sw-broadcast-live-badge">
|
<div className="sw-broadcast-live-badge">
|
||||||
<span className="sw-broadcast-dot" />
|
<span className="sw-broadcast-dot" />
|
||||||
<span>{viewerCount} watching</span>
|
<span>{viewerCount} watching</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="sw-broadcast-timer">
|
||||||
|
<i className="fa-solid fa-clock" />
|
||||||
|
<span>{formatTime(elapsedTime)}</span>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
|
|
@ -304,7 +374,7 @@ export default function LiveKitBroadcast({ coords, onClose, onStreamCreated }) {
|
||||||
{isLive && (
|
{isLive && (
|
||||||
<div className="sw-broadcast-live-indicator">
|
<div className="sw-broadcast-live-indicator">
|
||||||
<span className="sw-broadcast-dot" />
|
<span className="sw-broadcast-dot" />
|
||||||
<span>LIVE on LiveKit</span>
|
<span>LIVE</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
@ -347,14 +417,61 @@ export default function LiveKitBroadcast({ coords, onClose, onStreamCreated }) {
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{isLive && (
|
{isLive && (
|
||||||
|
<>
|
||||||
<div className="sw-broadcast-live-info">
|
<div className="sw-broadcast-live-info">
|
||||||
<h3 className="sw-broadcast-title">{title}</h3>
|
<h3 className="sw-broadcast-title">{title}</h3>
|
||||||
<div className="sw-broadcast-stats">
|
<div className="sw-broadcast-stats">
|
||||||
<span>
|
<span>
|
||||||
<i className="fa-solid fa-eye" /> {viewerCount} viewers
|
<i className="fa-solid fa-eye" /> {viewerCount} viewers
|
||||||
</span>
|
</span>
|
||||||
|
<span>
|
||||||
|
<i className="fa-solid fa-clock" /> {formatTime(elapsedTime)}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Comments section */}
|
||||||
|
<div className="sw-broadcast-comments">
|
||||||
|
<div className="sw-broadcast-comments-header">
|
||||||
|
<i className="fa-solid fa-comments" />
|
||||||
|
<span>Comments ({comments.length})</span>
|
||||||
|
</div>
|
||||||
|
<div className="sw-broadcast-comments-list">
|
||||||
|
{comments.length === 0 ? (
|
||||||
|
<div className="sw-broadcast-no-comments">
|
||||||
|
No comments yet
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
comments.map((c) => (
|
||||||
|
<div key={c.id} className="sw-broadcast-comment">
|
||||||
|
<span className="sw-broadcast-comment-author">{c.author}</span>
|
||||||
|
<span className="sw-broadcast-comment-text">{c.text}</span>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
<div ref={commentsEndRef} />
|
||||||
|
</div>
|
||||||
|
<div className="sw-broadcast-comment-input-row">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className="sw-broadcast-comment-input"
|
||||||
|
placeholder="Add a comment..."
|
||||||
|
value={commentInput}
|
||||||
|
onChange={(e) => setCommentInput(e.target.value)}
|
||||||
|
onKeyDown={(e) => e.key === "Enter" && handleAddComment()}
|
||||||
|
maxLength={200}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="sw-broadcast-comment-send"
|
||||||
|
onClick={handleAddComment}
|
||||||
|
disabled={!commentInput.trim()}
|
||||||
|
>
|
||||||
|
<i className="fa-solid fa-paper-plane" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{error && (
|
{error && (
|
||||||
|
|
@ -404,6 +521,11 @@ export default function LiveKitBroadcast({ coords, onClose, onStreamCreated }) {
|
||||||
<div className="sw-broadcast-confirm">
|
<div className="sw-broadcast-confirm">
|
||||||
<h3>End Stream?</h3>
|
<h3>End Stream?</h3>
|
||||||
<p>Your live stream will end for all viewers.</p>
|
<p>Your live stream will end for all viewers.</p>
|
||||||
|
{comments.length > 0 && (
|
||||||
|
<p className="sw-broadcast-confirm-comments">
|
||||||
|
<i className="fa-solid fa-comments" /> {comments.length} comments will be saved
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
<div className="sw-broadcast-confirm-actions">
|
<div className="sw-broadcast-confirm-actions">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
|
@ -417,14 +539,14 @@ export default function LiveKitBroadcast({ coords, onClose, onStreamCreated }) {
|
||||||
className="sw-broadcast-btn danger"
|
className="sw-broadcast-btn danger"
|
||||||
onClick={() => handleEndStream(false)}
|
onClick={() => handleEndStream(false)}
|
||||||
>
|
>
|
||||||
End Stream
|
Discard
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="sw-broadcast-btn primary"
|
className="sw-broadcast-btn primary"
|
||||||
onClick={() => handleEndStream(true)}
|
onClick={() => handleEndStream(true)}
|
||||||
>
|
>
|
||||||
End & Save
|
Save
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -208,6 +208,32 @@ export function useLiveKit({ roomName, userID, username, token: authToken }) {
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// Replace video track (for camera switch)
|
||||||
|
const replaceVideoTrack = useCallback(async (newTrack) => {
|
||||||
|
if (!roomRef.current || !newTrack) return false;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Unpublish old track
|
||||||
|
if (localVideoTrackRef.current) {
|
||||||
|
await roomRef.current.localParticipant.unpublishTrack(localVideoTrackRef.current);
|
||||||
|
localVideoTrackRef.current.stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Publish new track
|
||||||
|
await roomRef.current.localParticipant.publishTrack(newTrack, {
|
||||||
|
name: "camera",
|
||||||
|
simulcast: true,
|
||||||
|
videoCodec: "vp8",
|
||||||
|
});
|
||||||
|
localVideoTrackRef.current = newTrack;
|
||||||
|
console.log("[LiveKit] Video track replaced");
|
||||||
|
return true;
|
||||||
|
} catch (err) {
|
||||||
|
console.error("[LiveKit] Failed to replace video track:", err);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
// Cleanup on unmount
|
// Cleanup on unmount
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return () => {
|
return () => {
|
||||||
|
|
@ -227,6 +253,7 @@ export function useLiveKit({ roomName, userID, username, token: authToken }) {
|
||||||
stopBroadcast,
|
stopBroadcast,
|
||||||
toggleAudio,
|
toggleAudio,
|
||||||
toggleVideo,
|
toggleVideo,
|
||||||
|
replaceVideoTrack,
|
||||||
room: roomRef.current,
|
room: roomRef.current,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue