time-mirror
コードはほとんどChatGPTに書かせたblu3mo.icon
だからところどころ変だったんですね(constじゃなくてletだったり)takker.icon
wwwblu3mo.icon
これは不思議、なぜだろうblu3mo.icon
自分は設計とデバッグだけ
「映像をローカルに保存する形でこれを作りたい」というところまでは自分が決めた()
今のところ、10秒前の状態を表示するみたい?takker.icon
理想は10分前くらいなのかな
今(2023/6/10)の実装:再生を始めた所から再生しつ続けるblu3mo.icon
なので、特定のn分前を再生するのではない
動画の再生バーを操作すれば、任意のタイミングの過去の映像を見る事が出来る
なるほどtakker.icon
あー、てことは、getDisplayMedia()で取得したデータをそのまま流すだけでいいみたいですね
ならかなり小規模なコードで済むはずです
seekableにできないかな
currentTimeに大きな値を入れる方法は、ライブストリーミングだと不能
適宜blob URLを差し替える以外に方法がなさそう
こいつかtakker.icon
ざっくり以下のコードのようなイメージ
code: js
useEffect(() => {
mediaSource.current.addEventListener('sourceopen', () => {
sourceBuffer.current = mediaSource.current.addSourceBuffer('video/webm;codecs=opus,vp8');
});
}, []);
useInterval(() => {
if (sourceBuffer.current && sourceBuffer.current.updating === false) {
let blob = new Blob(recordedChunks, { type: 'video/webm' });
blob.arrayBuffer().then(data => {
sourceBuffer.current.appendBuffer(data);
});
setRecordedChunks([]);
}
}, isPlaying ? 1000 : null);
ただ、色々エラーにぶつかって上手くいっていないblu3mo.icon
再生バー操作ができなくなってもいいなら、時間差でcanvasに描画した画像をvideo要素にstreamingする手がありますtakker.icon
再生バーをDOMで構築するなら操作もできるかも
過去の映像をメモリにblobで保存しておき、巻き戻し操作が発生したらそれを呼び出して再生する
video側でのseekは依然不能だが、どうせPiPだとunseekableなので気にする必要はない
cursor: "always"は無効ですtakker.icon
昔はあったらしいけど、Opera以外サポートしていなかった
UserScript版
多分動く
動かないや。なんかミスったな
仕組み上reactを使う必要がないので消す
code:script.ts
/// <reference no-default-lib="true" />
/// <reference lib="esnext" />
/// <reference lib="dom" />
declare const scrapbox: Scrapbox;
let video: HTMLVideoElement | undefined;
scrapbox.PageMenu.addItem({
title: "recording",
onClick: async () => {
if (video) return;
const stream = await navigator.mediaDevices.getDisplayMedia({
video: true,
audio: false,
});
video = document.createElement("video");
video.style.maxWidth = "50%";
video.style.position = "fixed";
video.style.bottom = "5%";
video.style.left = "5%";
video.controls = true;
video.autoplay = true;
video.srcObject = stream;
video.currentTime = 7*24*60*1000;
video.onseeked = ()=>{
alert("after-seeked: "+video.duration);
video.onseeked = undefined;
};
document.body.append(video);
},
});
scrapbox.PageMenu.addItem({
title: "stop",
onClick: () => {
if (!video) return;
const stream = video.srcObject;
if (stream instanceof MediaStream) {
for (const track of stream.getTracks()) {
track.stop();
}
}
video.srcObject = null;
video.remove();
video = undefined;
},
});
code:mod.ts
/// <reference no-default-lib="true" />
/// <reference lib="esnext" />
/// <reference lib="dom" />
const stream = await navigator.mediaDevices.getDisplayMedia({
video: true,
audio: false,
});
const chunks: Blob[] = [];
const offscreen = document.createElement("canvas");
const ctx = offscreen.getContext("2d");
if (!ctx) throw Error("cannot get a contex from <canvas>");
const recorder = new MediaRecorder(stream);
let animationId: number | undefined;
recorder.addEventListener("dataavailable", async (event) => {
if (event.data.size === 0) return;
chunks.push(event.data);
const url = URL.createObjectURL(event.data);
const video = document.createElement("video");
video.src = url;
await new Promise(
(resolve) =>
video.addEventListener("loadedmetadata", resolve, { once: true }),
);
video.play();
offscreen.width = video.videoWidth;
offscreen.height = video.videoHeight;
const tick = () => {
ctx.drawImage(video, 0, 0);
animationId = requestAnimationFrame(tick);
};
tick();
});
recorder.start(5 * 1000); // collect data by 10min
return [
offscreen.captureStream(),
/** disposer */
() => {
recorder.stop();
recorder.stream.getTracks().forEach((track) => track.stop());
if (animationId !== undefined) cancelAnimationFrame(animationId);
},
] as const;
};