236 lines
8.2 KiB
JavaScript
236 lines
8.2 KiB
JavaScript
(function () {
|
|
if (typeof BridgeSwarmExamples === 'undefined') return;
|
|
|
|
var logEl = document.getElementById('log');
|
|
function log(msg, type) {
|
|
BridgeSwarmExamples.logLine(logEl, msg, type);
|
|
}
|
|
|
|
var live = document.getElementById('live');
|
|
var clip = document.getElementById('clip');
|
|
var out = document.getElementById('out');
|
|
var poster = document.getElementById('poster');
|
|
var recState = document.getElementById('recState');
|
|
var progress = document.getElementById('progress');
|
|
|
|
var captureStream = null;
|
|
var recorder = null;
|
|
var chunks = [];
|
|
var recordedBlob = null;
|
|
var lastJobPath = null;
|
|
var lastOutPath = null;
|
|
|
|
function setRec(label, livePill) {
|
|
recState.textContent = label || '';
|
|
recState.className = 'bs-pill' + (livePill ? ' bs-pill--live' : '');
|
|
}
|
|
|
|
function blobToBase64(blob) {
|
|
return new Promise(function (resolve, reject) {
|
|
var reader = new FileReader();
|
|
reader.onload = function () {
|
|
var s = String(reader.result);
|
|
var i = s.indexOf(',');
|
|
resolve(i >= 0 ? s.slice(i + 1) : s);
|
|
};
|
|
reader.onerror = reject;
|
|
reader.readAsDataURL(blob);
|
|
});
|
|
}
|
|
|
|
async function uploadBlob(blob, filename) {
|
|
var CHUNK = 500000; // ~base64 under NMH when chunked via multiple writeInput
|
|
var buf = await blob.arrayBuffer();
|
|
var bytes = new Uint8Array(buf);
|
|
var jobId = 'clip_' + Date.now();
|
|
var offset = 0;
|
|
var first = true;
|
|
while (offset < bytes.length) {
|
|
var end = Math.min(offset + CHUNK, bytes.length);
|
|
var slice = bytes.subarray(offset, end);
|
|
var binary = '';
|
|
for (var i = 0; i < slice.length; i++) binary += String.fromCharCode(slice[i]);
|
|
var dataBase64 = btoa(binary);
|
|
await window.BridgeSwarm.media.writeInput({
|
|
jobId: jobId,
|
|
filename: filename,
|
|
dataBase64: dataBase64,
|
|
append: !first,
|
|
});
|
|
first = false;
|
|
offset = end;
|
|
progress.textContent = 'Upload ' + Math.round((offset / bytes.length) * 100) + '%';
|
|
}
|
|
return { jobId: jobId, path: jobId + '/' + filename };
|
|
}
|
|
|
|
async function startCapture(kind) {
|
|
stopCapture();
|
|
try {
|
|
if (kind === 'screen') {
|
|
captureStream = await navigator.mediaDevices.getDisplayMedia({ video: true, audio: false });
|
|
} else {
|
|
captureStream = await navigator.mediaDevices.getUserMedia({ video: true, audio: false });
|
|
}
|
|
live.srcObject = captureStream;
|
|
document.getElementById('btnRecord').disabled = false;
|
|
document.getElementById('btnStopCapture').disabled = false;
|
|
log('Capture started (' + kind + ')', 'val');
|
|
captureStream.getVideoTracks()[0].addEventListener('ended', function () {
|
|
stopCapture();
|
|
});
|
|
} catch (e) {
|
|
log(e.message, 'err');
|
|
}
|
|
}
|
|
|
|
function stopCapture() {
|
|
if (recorder && recorder.state !== 'inactive') {
|
|
try { recorder.stop(); } catch (_) {}
|
|
}
|
|
if (captureStream) {
|
|
captureStream.getTracks().forEach(function (t) { t.stop(); });
|
|
captureStream = null;
|
|
}
|
|
live.srcObject = null;
|
|
document.getElementById('btnRecord').disabled = true;
|
|
document.getElementById('btnStopCapture').disabled = true;
|
|
document.getElementById('btnStopRecord').disabled = true;
|
|
setRec('');
|
|
}
|
|
|
|
function startRecord() {
|
|
if (!captureStream) return;
|
|
chunks = [];
|
|
recordedBlob = null;
|
|
var mime = MediaRecorder.isTypeSupported('video/webm;codecs=vp9')
|
|
? 'video/webm;codecs=vp9'
|
|
: 'video/webm';
|
|
try {
|
|
recorder = new MediaRecorder(captureStream, { mimeType: mime });
|
|
} catch (e) {
|
|
recorder = new MediaRecorder(captureStream);
|
|
}
|
|
recorder.ondataavailable = function (e) {
|
|
if (e.data && e.data.size) chunks.push(e.data);
|
|
};
|
|
recorder.onstop = function () {
|
|
recordedBlob = new Blob(chunks, { type: recorder.mimeType || 'video/webm' });
|
|
clip.src = URL.createObjectURL(recordedBlob);
|
|
document.getElementById('clipStage').hidden = false;
|
|
document.getElementById('btnProcess').disabled = false;
|
|
document.getElementById('btnPoster').disabled = false;
|
|
setRec('Recorded ' + Math.round(recordedBlob.size / 1024) + ' KB');
|
|
log('Recording ready (' + recordedBlob.size + ' bytes)', 'val');
|
|
};
|
|
var maxMs = (Number(document.getElementById('maxSec').value) || 8) * 1000;
|
|
recorder.start(250);
|
|
document.getElementById('btnRecord').disabled = true;
|
|
document.getElementById('btnStopRecord').disabled = false;
|
|
setRec('Recording…', true);
|
|
log('Recording up to ' + (maxMs / 1000) + 's', 'sys');
|
|
setTimeout(function () {
|
|
if (recorder && recorder.state === 'recording') stopRecord();
|
|
}, maxMs);
|
|
}
|
|
|
|
function stopRecord() {
|
|
if (!recorder || recorder.state === 'inactive') return;
|
|
recorder.stop();
|
|
document.getElementById('btnRecord').disabled = !captureStream;
|
|
document.getElementById('btnStopRecord').disabled = true;
|
|
}
|
|
|
|
BridgeSwarmExamples.waitForBridgeSwarm().then(async function () {
|
|
var has = await window.BridgeSwarm.capabilities.has('media');
|
|
if (!has) {
|
|
document.getElementById('missing').hidden = false;
|
|
log('Media capability missing', 'err');
|
|
return;
|
|
}
|
|
log('Media pack ready', 'val');
|
|
|
|
document.getElementById('btnScreen').onclick = function () { startCapture('screen'); };
|
|
document.getElementById('btnCamera').onclick = function () { startCapture('camera'); };
|
|
document.getElementById('btnStopCapture').onclick = stopCapture;
|
|
document.getElementById('btnRecord').onclick = startRecord;
|
|
document.getElementById('btnStopRecord').onclick = stopRecord;
|
|
|
|
document.getElementById('btnProcess').onclick = async function () {
|
|
if (!recordedBlob) return;
|
|
try {
|
|
progress.textContent = 'Uploading…';
|
|
var up = await uploadBlob(recordedBlob, 'clip.webm');
|
|
lastJobPath = up.path;
|
|
log('Uploaded to ' + up.path, 'sys');
|
|
progress.textContent = 'Transcoding…';
|
|
var r = await window.BridgeSwarm.media.transcode(
|
|
{ path: up.path, format: 'webm', width: 640, height: 360, jobId: up.jobId + '_x' },
|
|
{
|
|
timeoutMs: 300000,
|
|
onProgress: function (p) {
|
|
if (p.bytes) progress.textContent = Math.round(p.bytes / 1024) + ' KB encoded';
|
|
},
|
|
}
|
|
);
|
|
lastOutPath = r.path;
|
|
progress.textContent = 'done';
|
|
log('Transcode → ' + (r.path || ''), 'val');
|
|
if (r.path) {
|
|
var read = await window.BridgeSwarm.media.readOutput({ path: r.path, mimetype: 'video/webm' });
|
|
out.src = 'data:video/webm;base64,' + read.dataBase64;
|
|
document.getElementById('outStage').hidden = false;
|
|
document.getElementById('btnDrive').disabled = false;
|
|
}
|
|
} catch (e) {
|
|
progress.textContent = '';
|
|
log(e.message, 'err');
|
|
}
|
|
};
|
|
|
|
document.getElementById('btnPoster').onclick = async function () {
|
|
if (!recordedBlob && !lastJobPath) return;
|
|
try {
|
|
var path = lastJobPath;
|
|
var jobId;
|
|
if (!path) {
|
|
var up = await uploadBlob(recordedBlob, 'clip.webm');
|
|
path = up.path;
|
|
jobId = up.jobId;
|
|
lastJobPath = path;
|
|
}
|
|
progress.textContent = 'Extracting frame…';
|
|
var r = await window.BridgeSwarm.media.extractFrame({
|
|
path: path,
|
|
frameIndex: 0,
|
|
mimetype: 'image/webp',
|
|
jobId: (jobId || 'poster') + '_f',
|
|
});
|
|
poster.src = 'data:' + (r.mimetype || 'image/webp') + ';base64,' + r.dataBase64;
|
|
document.getElementById('posterStage').hidden = false;
|
|
progress.textContent = '';
|
|
log('Poster frame ready', 'val');
|
|
} catch (e) {
|
|
progress.textContent = '';
|
|
log(e.message, 'err');
|
|
}
|
|
};
|
|
|
|
document.getElementById('btnDrive').onclick = async function () {
|
|
if (!lastOutPath) return;
|
|
try {
|
|
var read = await window.BridgeSwarm.media.readOutput({ path: lastOutPath, mimetype: 'video/webm' });
|
|
await window.BridgeSwarm.request('drivePut', {
|
|
path: '/clips/' + Date.now() + '.webm',
|
|
base64: read.dataBase64,
|
|
});
|
|
log('Saved to Hyperdrive /clips/…', 'val');
|
|
} catch (e) {
|
|
log(e.message, 'err');
|
|
}
|
|
};
|
|
}).catch(function (e) {
|
|
log(e.message, 'err');
|
|
});
|
|
})();
|