Skip to content
Merged
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
1 change: 1 addition & 0 deletions backend/node_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -628,6 +628,7 @@ def generate_csv_template() -> str:
"resources": json.dumps(
{
"session_data": {"device": "metadata"},
"video_ws_url": "ws://10.160.13.110:8099/stream/ios/00008020-001C2D113C88002E",
"stf": {
"base_url": "https://stf.example.com",
"control_path_template": "/#!/control/{udid}",
Expand Down
2 changes: 1 addition & 1 deletion backend/node_resources_template.csv
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
id,type,udid,host,port,protocol,path,max_sessions,active_sessions,status,platform,platform_version,device_name,resources
example-node-id,physical,00008020-001C2D113C88002E,127.0.0.1,4723,http,/wd/hub,1,0,online,iOS,17.4,Example Device,"{""session_data"": {""device"": ""metadata""}}"
example-node-id,physical,00008020-001C2D113C88002E,127.0.0.1,4723,http,/wd/hub,1,0,online,iOS,17.4,Example Device,"{""session_data"": {""device"": ""metadata""}, ""video_ws_url"": ""ws://10.160.13.110:8099/stream/ios/00008020-001C2D113C88002E""}"
10 changes: 10 additions & 0 deletions frontend/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -523,6 +523,16 @@ function deriveStreamUrl(node) {
return null;
}

// First, try to get video_ws_url from node resources
const resources = node.resources;
if (resources && typeof resources === 'object') {
const videoWsUrl = resources.video_ws_url;
if (typeof videoWsUrl === 'string' && videoWsUrl.trim()) {
return videoWsUrl.trim();
}
}

// Fallback to legacy stream URL construction
const udid = typeof node.udid === 'string' ? node.udid.trim() : '';
if (!udid) {
return null;
Expand Down
15 changes: 15 additions & 0 deletions frontend/embed.html
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,21 @@
width="212"
height="472"
/>
<canvas
id="embed-stream-canvas"
class="embed-stream"
alt="Device stream"
style="display: none; -webkit-user-select: none; margin: auto; background-color: hsl(0, 0%, 25%);"
width="212"
height="472"
></canvas>
<div
id="embed-placeholder"
class="embed-placeholder"
hidden
>
<p class="embed-placeholder__message">Loading stream...</p>
</div>
</div>
</div>
</div>
Expand Down
89 changes: 81 additions & 8 deletions frontend/embed.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,14 @@
const label = (params.get('label') || params.get('title') || '').trim();
const subtitle = (params.get('subtitle') || params.get('description') || '').trim();
const streamImage = document.getElementById('embed-stream');
const streamCanvas = document.getElementById('embed-stream-canvas');
const placeholder = document.getElementById('embed-placeholder');
const labelEl = document.getElementById('embed-label');
const subtitleEl = document.getElementById('embed-subtitle');

let ws = null;
let canvasCtx = null;

function pickFirstValue(values) {
return values.find((value) => value && value.trim());
}
Expand All @@ -26,11 +30,80 @@
if (message) {
const messageNode = placeholder.querySelector('.embed-placeholder__message');
if (messageNode) {
messageNode.innerHTML = message;
messageNode.textContent = message;
}
}
placeholder.hidden = false;
streamImage.hidden = true;
streamCanvas.style.display = 'none';
}

function hideAll() {
placeholder.hidden = true;
streamImage.hidden = true;
streamCanvas.style.display = 'none';
}

function isWebSocketUrl(url) {
return url.startsWith('ws://') || url.startsWith('wss://');
}

function connectWebSocket(url) {
hideAll();
showPlaceholder('Connecting to stream...');

ws = new WebSocket(url);
ws.binaryType = 'arraybuffer';

ws.onopen = () => {
console.log('WebSocket connected');
placeholder.hidden = true;
streamCanvas.style.display = 'block';
canvasCtx = streamCanvas.getContext('2d');
};

ws.onmessage = (event) => {
if (event.data instanceof ArrayBuffer) {
const blob = new Blob([event.data], { type: 'image/jpeg' });
const urlCreator = window.URL || window.webkitURL;
const imageUrl = urlCreator.createObjectURL(blob);

const img = new Image();
img.onload = () => {
if (canvasCtx) {
// Auto-resize canvas to match image dimensions
if (streamCanvas.width !== img.width || streamCanvas.height !== img.height) {
streamCanvas.width = img.width;
streamCanvas.height = img.height;
}
canvasCtx.drawImage(img, 0, 0);
}
urlCreator.revokeObjectURL(imageUrl);
};
img.src = imageUrl;
}
};

ws.onerror = (error) => {
console.error('WebSocket error:', error);
showPlaceholder('WebSocket connection error. Please check the stream URL.');
};

ws.onclose = () => {
console.log('WebSocket closed');
showPlaceholder('Stream connection closed.');
};
}

function loadHttpStream(url) {
hideAll();
streamImage.src = url;
streamImage.hidden = false;
streamImage.addEventListener('error', () => {
showPlaceholder(
'We were unable to load the requested stream. Please check that the URL is reachable.'
);
});
}

if (label) {
Expand All @@ -48,19 +121,19 @@

if (!srcToUse) {
showPlaceholder('No stream URL was provided.');
} else if (isWebSocketUrl(srcToUse)) {
connectWebSocket(srcToUse);
} else {
streamImage.src = srcToUse;
streamImage.hidden = false;
streamImage.addEventListener('error', () => {
showPlaceholder(
'We were unable to load the requested stream. Please check that the URL is reachable.'
);
});
loadHttpStream(srcToUse);
}

const closeTriggers = document.querySelectorAll('[data-close]');
closeTriggers.forEach((trigger) => {
trigger.addEventListener('click', () => {
if (ws) {
ws.close();
}

if (window.history.length > 1) {
window.history.back();
return;
Expand Down