feat(recordings): enhance recording management with improved error handling, finalize recording logic, and add motion notification support

This commit is contained in:
2026-02-03 17:45:00 +00:00
parent ef74b5ca19
commit 23db01dfc8
4 changed files with 442 additions and 39 deletions

View File

@@ -208,17 +208,6 @@
<!-- CLIENT DASHBOARD -->
<section id="screen-home-client" class="hidden space-y-5">
<div class="relative overflow-hidden rounded-2xl bg-gray-900 border border-white/5 aspect-video">
<video id="clientStreamVideo" class="absolute inset-0 w-full h-full object-cover hidden" autoplay playsinline></video>
<img id="clientStreamImage" class="absolute inset-0 w-full h-full object-cover hidden" alt="Live stream preview" />
<div id="clientStreamPlaceholder" class="absolute inset-0 flex flex-col items-center justify-center gap-2 text-gray-500">
<svg xmlns="http://www.w3.org/2000/svg" class="h-8 w-8 text-gray-700" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 10l4.553-2.276A1 1 0 0121 8.618v6.764a1 1 0 01-1.447.894L15 14M5 18h8a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v8a2 2 0 002 2z" />
</svg>
<p class="text-xs">No active live stream</p>
</div>
</div>
<!-- Quick Actions -->
<div class="flex gap-2 overflow-x-auto pb-1 scrollbar-hide">
<button id="linkCameraBtn" class="btn btn-sm btn-outline border-white/10 text-gray-300 gap-2 shrink-0">
@@ -233,7 +222,7 @@
<!-- Live Feeds Section -->
<div class="space-y-3">
<h3 class="text-xs font-bold text-gray-500 uppercase tracking-wider pl-1">Linked Cameras</h3>
<div id="linkedCamerasList" class="space-y-3">
<div id="linkedCamerasList" class="flex gap-3 overflow-x-auto pb-2 scrollbar-hide">
<!-- Populated by JS -->
<div class="text-center py-8 bg-gray-900/30 rounded-xl border border-dashed border-gray-800">
<p class="text-gray-600 text-xs">No cameras linked yet</p>
@@ -309,5 +298,15 @@
<script src="/socket.io/socket.io.js"></script>
<script src="/sim/mobile-sim.js" defer></script>
<div id="recordingModal" class="fixed inset-0 bg-black/70 backdrop-blur-sm z-50 hidden items-center justify-center p-4">
<div class="w-full max-w-lg bg-gray-900 border border-white/10 rounded-2xl p-4 space-y-3 shadow-2xl">
<div class="flex items-center justify-between">
<h3 id="recordingModalTitle" class="text-sm font-semibold text-white">Recording Playback</h3>
<button id="recordingModalCloseBtn" class="btn btn-xs btn-ghost text-gray-400">Close</button>
</div>
<video id="recordingModalVideo" class="w-full rounded-lg bg-black max-h-[60vh]" controls playsinline></video>
</div>
</div>
</body>
</html>

View File

@@ -39,6 +39,9 @@ const store = new Store({
cameraStatus: 'idle', // idle, recording, streaming
linkedCameras: [],
recordings: [],
motionNotifications: [],
activeCameraDeviceId: null,
activeStreamSessionId: null,
activityFeed: [],
loading: false, // global loading spinner state if needed
});
@@ -131,7 +134,7 @@ const API = {
events: {
startMotion: () => API.request('/events/motion/start', { method: 'POST', body: JSON.stringify({ title: 'Simulated Motion', triggeredBy: 'motion' }) }),
endMotion: (id) => API.request(`/events/${id}/motion/end`, { method: 'POST', body: JSON.stringify({ status: 'completed' }) }),
finalizeRecording: (id, objectKey) => API.request(`/recordings/${id}/finalize`, { method: 'POST', body: JSON.stringify({ objectKey, bucket: 'videos', durationSeconds: 15, sizeBytes: 5000000 }) }),
finalizeRecording: (id, payload) => API.request(`/recordings/${id}/finalize`, { method: 'POST', body: JSON.stringify(payload) }),
},
ops: {
@@ -154,6 +157,10 @@ let remoteStreamWaitTimer = null;
let frameRelayTimer = null;
let frameCanvas = null;
let frameContext = null;
let activeMediaRecorder = null;
let activeRecordingChunks = [];
let activeRecordingStartedAt = null;
let recordingModalUrl = null;
const rtcConfig = {
iceServers: [{ urls: 'stun:stun.l.google.com:19302' }],
};
@@ -269,6 +276,70 @@ const clearClientStream = () => {
setClientStreamVisibility(false);
};
const getCameraLabel = (cameraDeviceId) => `Camera ${cameraDeviceId?.substring(0, 6) ?? 'Unknown'}`;
const pushMotionNotification = (cameraDeviceId) => {
if (!cameraDeviceId) return;
const notification = {
id: crypto.randomUUID(),
cameraDeviceId,
message: `${getCameraLabel(cameraDeviceId)} has detected movement`,
createdAt: new Date().toISOString(),
isRead: false,
};
store.update({
motionNotifications: [notification, ...store.get().motionNotifications].slice(0, 50),
});
};
const markMotionNotificationRead = (notificationId) => {
store.update({
motionNotifications: store
.get()
.motionNotifications.map((notification) =>
notification.id === notificationId ? { ...notification, isRead: true } : notification,
),
});
};
const markAllNotificationsRead = () => {
store.update({
motionNotifications: store
.get()
.motionNotifications.map((notification) => (notification.isRead ? notification : { ...notification, isRead: true })),
});
};
const openRecordingModal = (downloadUrl, title) => {
const modal = $('recordingModal');
const videoEl = $('recordingModalVideo');
const titleEl = $('recordingModalTitle');
if (!modal || !videoEl || !titleEl) return;
recordingModalUrl = downloadUrl;
titleEl.textContent = title || 'Recording Playback';
videoEl.src = downloadUrl;
modal.classList.remove('hidden');
modal.classList.add('flex');
void videoEl.play().catch(() => {});
};
const closeRecordingModal = () => {
const modal = $('recordingModal');
const videoEl = $('recordingModalVideo');
if (!modal || !videoEl) return;
modal.classList.add('hidden');
modal.classList.remove('flex');
videoEl.pause();
videoEl.removeAttribute('src');
videoEl.load();
recordingModalUrl = null;
};
const stopFrameRelay = () => {
if (frameRelayTimer) {
clearInterval(frameRelayTimer);
@@ -311,6 +382,82 @@ const startFrameRelay = async (streamSessionId, toDeviceId) => {
}, 300);
};
const getPreferredRecordingMimeType = () => {
if (typeof MediaRecorder === 'undefined') return '';
const preferredTypes = [
'video/webm;codecs=vp9',
'video/webm;codecs=vp8',
'video/webm',
];
return preferredTypes.find((type) => MediaRecorder.isTypeSupported(type)) ?? '';
};
const startLocalRecording = async () => {
if (!localCameraStream || typeof MediaRecorder === 'undefined') {
addActivity('Recording', 'MediaRecorder unavailable');
return false;
}
if (activeMediaRecorder?.state === 'recording') {
return true;
}
activeRecordingChunks = [];
activeRecordingStartedAt = Date.now();
try {
const mimeType = getPreferredRecordingMimeType();
activeMediaRecorder = mimeType ? new MediaRecorder(localCameraStream, { mimeType }) : new MediaRecorder(localCameraStream);
} catch (error) {
console.error('Failed to create MediaRecorder', error);
addActivity('Recording', 'Failed to start recorder');
activeMediaRecorder = null;
return false;
}
activeMediaRecorder.ondataavailable = (event) => {
if (event.data && event.data.size > 0) {
activeRecordingChunks.push(event.data);
}
};
activeMediaRecorder.start(1000);
addActivity('Recording', 'Local recording started');
return true;
};
const stopLocalRecording = async () => {
if (!activeMediaRecorder || activeMediaRecorder.state === 'inactive') {
return null;
}
return await new Promise((resolve) => {
const recorder = activeMediaRecorder;
const startedAt = activeRecordingStartedAt ?? Date.now();
recorder.onstop = () => {
const mimeType = recorder.mimeType || 'video/webm';
const blob = activeRecordingChunks.length > 0 ? new Blob(activeRecordingChunks, { type: mimeType }) : null;
const durationSeconds = Math.max(1, Math.round((Date.now() - startedAt) / 1000));
activeMediaRecorder = null;
activeRecordingChunks = [];
activeRecordingStartedAt = null;
resolve(blob ? { blob, durationSeconds } : null);
};
recorder.onerror = () => {
activeMediaRecorder = null;
activeRecordingChunks = [];
activeRecordingStartedAt = null;
resolve(null);
};
recorder.stop();
});
};
const teardownPeerConnection = () => {
if (peerConnection) {
peerConnection.onicecandidate = null;
@@ -408,6 +555,73 @@ const startOfferToClient = async (streamSessionId, requesterDeviceId) => {
});
};
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
const finalizeRecordingForStream = async (streamSessionId, captureResult) => {
const currentDevice = store.get().device;
if (!currentDevice?.id) {
addActivity('Recording', 'No device identity for finalize');
return false;
}
for (let attempt = 0; attempt < 8; attempt += 1) {
const recs = await API.ops.listRecordings().catch(() => ({ recordings: [] }));
const recording = (recs.recordings || []).find((rec) => rec.streamSessionId === streamSessionId && rec.status === 'awaiting_upload');
if (recording?.id) {
try {
if (!captureResult?.blob || captureResult.blob.size === 0) {
throw new Error('No captured video blob to upload');
}
const uploadMeta = await API.request('/videos/upload-url', {
method: 'POST',
body: JSON.stringify({
fileName: `stream-${streamSessionId}.webm`,
deviceId: currentDevice.id,
prefix: 'recordings',
}),
});
const uploadResponse = await fetch(uploadMeta.uploadUrl, {
method: 'PUT',
headers: { 'Content-Type': captureResult.blob.type || 'video/webm' },
body: captureResult.blob,
});
if (!uploadResponse.ok) {
throw new Error(`Upload failed with status ${uploadResponse.status}`);
}
await API.events.finalizeRecording(recording.id, {
objectKey: uploadMeta.objectKey,
bucket: uploadMeta.bucket,
durationSeconds: captureResult.durationSeconds,
sizeBytes: captureResult.blob.size,
});
addActivity('Recording', 'Recording uploaded and finalized');
return true;
} catch (error) {
console.error('Recording upload failed, falling back to simulated key', error);
const fallbackObjectKey = `sim/${streamSessionId}/${Date.now()}.webm`;
await API.events.finalizeRecording(recording.id, {
objectKey: fallbackObjectKey,
durationSeconds: captureResult?.durationSeconds ?? 15,
sizeBytes: captureResult?.blob?.size ?? 5000000,
});
addActivity('Recording', 'Upload failed; finalized with simulator fallback');
return true;
}
}
await sleep(350);
}
addActivity('Recording', 'No recording row found to finalize');
return false;
};
const connectSocket = () => {
const { deviceToken } = store.get();
if (!deviceToken) return;
@@ -427,7 +641,9 @@ const connectSocket = () => {
socket.on('disconnect', () => {
store.update({ socketConnected: false });
stopFrameRelay();
void stopLocalRecording();
teardownPeerConnection();
store.update({ activeCameraDeviceId: null, activeStreamSessionId: null });
});
// Handle commands (as Camera)
@@ -443,6 +659,7 @@ const connectSocket = () => {
}
await API.streams.accept(streamId);
await API.streams.getPublishCreds(streamId);
await startLocalRecording();
if (payload.sourceDeviceId) {
await startOfferToClient(streamId, payload.sourceDeviceId);
await startFrameRelay(streamId, payload.sourceDeviceId);
@@ -450,7 +667,9 @@ const connectSocket = () => {
addActivity('Stream', 'Accepted & Published');
// Auto-stop after 15s for simulation
setTimeout(async () => {
const captureResult = await stopLocalRecording();
await API.streams.end(streamId);
await finalizeRecordingForStream(streamId, captureResult);
stopFrameRelay();
if (socket && payload.sourceDeviceId) {
socket.emit('webrtc:signal', {
@@ -460,6 +679,7 @@ const connectSocket = () => {
});
}
teardownPeerConnection();
store.update({ activeCameraDeviceId: null, activeStreamSessionId: null });
addActivity('Stream', 'Ended auto-simulation');
}, 15000);
}
@@ -472,14 +692,19 @@ const connectSocket = () => {
// Handle Events (as Client)
socket.on('motion:detected', (payload) => {
addActivity('Motion', `Detected on camera ${payload.deviceId?.split('-')[0]}...`);
const cameraDeviceId = payload.cameraDeviceId || payload.deviceId;
addActivity('Motion', `${getCameraLabel(cameraDeviceId)} has detected movement`);
Toast.show('Motion Detected!', 'info');
updateNotificationDot(true);
pushMotionNotification(cameraDeviceId);
});
socket.on('stream:started', async (payload) => {
addActivity('Stream', 'Stream is live, connecting...');
clearClientStream();
store.update({
activeCameraDeviceId: payload.cameraDeviceId ?? store.get().activeCameraDeviceId,
activeStreamSessionId: payload.streamSessionId ?? null,
});
try {
await API.streams.getSubscribeCreds(payload.streamSessionId);
Toast.show('Connected to Stream', 'success');
@@ -511,6 +736,13 @@ const connectSocket = () => {
setClientStreamVisibility(true);
});
socket.on('stream:ended', (payload) => {
if (payload?.streamSessionId && payload.streamSessionId === store.get().activeStreamSessionId) {
clearClientStream();
store.update({ activeCameraDeviceId: null, activeStreamSessionId: null });
}
});
socket.on('webrtc:signal', async (payload) => {
const device = store.get().device;
if (!device || !payload?.streamSessionId || !payload?.signalType || !payload?.fromDeviceId) return;
@@ -553,6 +785,7 @@ const connectSocket = () => {
if (payload.signalType === 'hangup') {
teardownPeerConnection();
store.update({ activeCameraDeviceId: null, activeStreamSessionId: null });
addActivity('Stream', 'Remote stream ended');
}
} catch (error) {
@@ -688,6 +921,7 @@ const Actions = {
store.update({ session: null, screen: 'auth', device: null, deviceToken: null, socketConnected: false });
if (socket) socket.disconnect();
stopFrameRelay();
await stopLocalRecording();
teardownPeerConnection();
stopCameraPreview();
localStorage.removeItem('mobileSimDevice');
@@ -728,6 +962,7 @@ const Actions = {
requestStream: async (camId) => {
try {
store.update({ activeCameraDeviceId: camId });
Toast.show('Requesting Stream...', 'info');
await API.streams.request(camId);
// Socket will handle the rest ('stream:started')
@@ -741,11 +976,35 @@ const Actions = {
Toast.show('Recording URL unavailable', 'error');
return;
}
window.open(result.downloadUrl, '_blank', 'noopener,noreferrer');
const recording = store.get().recordings.find((entry) => entry.id === recordingId);
const title = recording ? `${new Date(recording.createdAt).toLocaleString()} recording` : 'Recording Playback';
openRecordingModal(result.downloadUrl, title);
} catch (e) {
// handled by API wrapper
}
},
closeRecordingModal: () => {
closeRecordingModal();
},
openMotionNotificationTarget: async (notificationId, cameraDeviceId) => {
markMotionNotificationRead(notificationId);
if (!cameraDeviceId) return;
const recs = await API.ops.listRecordings().catch(() => ({ recordings: [] }));
const readyRecording = (recs.recordings || [])
.filter((recording) => recording.cameraDeviceId === cameraDeviceId && recording.status === 'ready')
.sort((left, right) => new Date(right.createdAt).getTime() - new Date(left.createdAt).getTime())[0];
if (readyRecording?.id) {
await Actions.openRecording(readyRecording.id);
return;
}
store.update({ screen: 'home' });
await Actions.requestStream(cameraDeviceId);
},
};
// --- 5. Rendering ---
@@ -783,6 +1042,9 @@ const render = (state) => {
// 3. Bottom Nav Visibility & State
const nav = $('bottomNav');
const unreadNotifications = state.motionNotifications.filter((notification) => !notification.isRead).length;
updateNotificationDot(unreadNotifications > 0);
if (state.session && state.device) {
nav.classList.remove('hidden');
$$('.nav-btn').forEach(btn => {
@@ -820,24 +1082,56 @@ const render = (state) => {
// 5. Client Mode Lists
if (state.device?.role === 'client' && state.screen === 'home') {
if (!state.activeCameraDeviceId && state.linkedCameras.length > 0) {
void Actions.requestStream(state.linkedCameras[0].cameraDeviceId);
}
const list = $('linkedCamerasList');
if (state.linkedCameras.length === 0) {
list.innerHTML = `<div class="text-center py-8 bg-gray-900/30 rounded-xl border border-dashed border-gray-800"><p class="text-gray-600 text-xs">No cameras linked yet</p></div>`;
list.innerHTML = `<div class="min-w-full text-center py-8 bg-gray-900/30 rounded-xl border border-dashed border-gray-800"><p class="text-gray-600 text-xs">No cameras linked yet</p></div>`;
} else {
list.innerHTML = state.linkedCameras.map(link => `
<div class="flex items-center justify-between p-3 bg-gray-900/60 rounded-xl border border-white/5">
<div class="flex items-center gap-3">
<div class="w-8 h-8 rounded-full bg-blue-900/30 flex items-center justify-center text-blue-500">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 10l4.553-2.276A1 1 0 0121 8.618v6.764a1 1 0 01-1.447.894L15 14M5 18h8a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v8a2 2 0 002 2z" /></svg>
</div>
<div>
<p class="text-xs font-bold text-gray-300">Camera ${link.cameraDeviceId.substring(0, 6)}</p>
<p class="text-[10px] text-gray-500">${link.status}</p>
</div>
<div class="min-w-[240px] max-w-[240px] bg-gray-900/60 rounded-xl border border-white/5 overflow-hidden">
<div class="relative overflow-hidden bg-black/40 border-b border-white/5 aspect-video">
${
state.activeCameraDeviceId === link.cameraDeviceId
? `
<video id="clientStreamVideo" class="absolute inset-0 w-full h-full object-cover hidden" autoplay playsinline></video>
<img id="clientStreamImage" class="absolute inset-0 w-full h-full object-cover hidden" alt="Live stream preview" />
<div id="clientStreamPlaceholder" class="absolute inset-0 flex flex-col items-center justify-center gap-2 text-gray-500">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 text-gray-700" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 10l4.553-2.276A1 1 0 0121 8.618v6.764a1 1 0 01-1.447.894L15 14M5 18h8a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v8a2 2 0 002 2z" />
</svg>
<p class="text-[10px]">${state.activeStreamSessionId ? 'Connecting stream...' : 'Waiting for stream'}</p>
</div>
`
: `
<div class="absolute inset-0 flex flex-col items-center justify-center gap-2 text-gray-500">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 text-gray-700" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 10l4.553-2.276A1 1 0 0121 8.618v6.764a1 1 0 01-1.447.894L15 14M5 18h8a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v8a2 2 0 002 2z" />
</svg>
<p class="text-[10px]">Stand by</p>
</div>
`
}
</div>
<div class="px-3 py-2">
<div>
<p class="text-xs font-bold text-gray-300">${getCameraLabel(link.cameraDeviceId)}</p>
<p class="text-[10px] text-gray-500">${link.status}</p>
</div>
</div>
<button class="btn btn-xs btn-primary request-stream-btn" data-camera-device-id="${link.cameraDeviceId}">Live</button>
</div>
`).join('');
if (state.activeCameraDeviceId && remoteClientStream) {
const videoEl = $('clientStreamVideo');
if (videoEl && videoEl.srcObject !== remoteClientStream) {
videoEl.srcObject = remoteClientStream;
setClientStreamVisibility(true);
void videoEl.play().catch(() => {});
}
}
}
const recList = $('recordingsList');
@@ -858,6 +1152,27 @@ const render = (state) => {
}
}
if (state.screen === 'activity') {
const activityFeed = $('activityFeedList');
if (state.motionNotifications.length === 0) {
activityFeed.innerHTML = `
<div class="text-center py-10 opacity-50">
<svg xmlns="http://www.w3.org/2000/svg" class="h-10 w-10 mx-auto mb-2 text-gray-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9" />
</svg>
<p class="text-sm text-gray-500">No notifications yet</p>
</div>
`;
} else {
activityFeed.innerHTML = state.motionNotifications.map((notification) => `
<button class="w-full text-left p-3 rounded-lg border border-white/5 ${notification.isRead ? 'bg-gray-900/30' : 'bg-blue-900/20'} motion-notification-btn" data-notification-id="${notification.id}" data-camera-device-id="${notification.cameraDeviceId}">
<p class="text-xs font-medium text-gray-200">${notification.message}</p>
<p class="text-[10px] text-gray-500 mt-1">${new Date(notification.createdAt).toLocaleString()}</p>
</button>
`).join('');
}
}
// 6. Settings Screen
if (state.session?.user && state.screen === 'settings') {
$('profileName').textContent = state.session.user.name;
@@ -902,13 +1217,6 @@ $('loadSavedBtn').addEventListener('click', () => { /* Handle legacy loading if
$$('#screen-onboarding [data-role]').forEach((btn) => {
btn.addEventListener('click', () => Actions.selectRole(btn.dataset.role));
});
$('linkedCamerasList').addEventListener('click', (event) => {
const target = event.target.closest('.request-stream-btn');
if (!target) return;
const cameraDeviceId = target.dataset.cameraDeviceId;
if (!cameraDeviceId) return;
Actions.requestStream(cameraDeviceId);
});
$('recordingsList').addEventListener('click', (event) => {
const target = event.target.closest('.download-recording-btn');
if (!target || target.disabled) return;
@@ -916,12 +1224,22 @@ $('recordingsList').addEventListener('click', (event) => {
if (!recordingId) return;
Actions.openRecording(recordingId);
});
$('activityFeedList').addEventListener('click', (event) => {
const target = event.target.closest('.motion-notification-btn');
if (!target) return;
const notificationId = target.dataset.notificationId;
const cameraDeviceId = target.dataset.cameraDeviceId;
if (!notificationId || !cameraDeviceId) return;
Actions.openMotionNotificationTarget(notificationId, cameraDeviceId);
});
// Navbar
$$('.nav-btn').forEach(btn => {
btn.addEventListener('click', () => {
if (btn.dataset.target === 'activity') {
markAllNotificationsRead();
}
store.update({ screen: btn.dataset.target });
if (btn.dataset.target === 'activity') updateNotificationDot(false);
});
});
@@ -941,6 +1259,15 @@ $('refreshClientBtn').addEventListener('click', startPolling);
// Settings
$('signOutBtn').addEventListener('click', Actions.signOut);
$('clearActivityBtn').addEventListener('click', () => {
store.update({ motionNotifications: [] });
});
$('recordingModalCloseBtn').addEventListener('click', Actions.closeRecordingModal);
$('recordingModal').addEventListener('click', (event) => {
if (event.target === $('recordingModal')) {
Actions.closeRecordingModal();
}
});
// Init
store.subscribe(render);
@@ -948,6 +1275,7 @@ init();
window.addEventListener('beforeunload', () => {
stopFrameRelay();
void stopLocalRecording();
teardownPeerConnection();
stopCameraPreview();
});