Loading apps/demo/package.json +1 −0 Original line number Diff line number Diff line Loading @@ -36,6 +36,7 @@ "next": "13.1.1", "react": "^18.2.0", "react-dom": "^18.2.0", "webm-muxer": "^1.1.0", "xterm": "^5.1.0", "xterm-addon-fit": "^0.7.0", "xterm-addon-search": "^0.11.0", Loading apps/demo/src/components/scrcpy/command-bar.tsx +34 −8 Original line number Diff line number Diff line Loading @@ -14,7 +14,8 @@ import { GlobalState } from "../../state"; import { Icons } from "../../utils"; import { ExternalLink } from "../external-link"; import { CommandBarSpacerItem } from "./command-bar-spacer-item"; import { Recorder, STATE } from "./state"; import { RECORD_STATE } from "./recorder"; import { STATE } from "./state"; const ITEMS = computed(() => { const result: ICommandBarItemProps[] = []; Loading @@ -36,13 +37,38 @@ const ITEMS = computed(() => { }); } result.push({ result.push( RECORD_STATE.recording ? { key: "Record", iconProps: { iconName: Icons.Record, style: { color: "red" }, }, // prettier-ignore text: `${ RECORD_STATE.hours ? `${RECORD_STATE.hours}:` : "" }${ RECORD_STATE.minutes.toString().padStart(2, "0") }:${ RECORD_STATE.seconds.toString().padStart(2, "0") }`, onClick: action(() => { RECORD_STATE.recorder.stop(); RECORD_STATE.recording = false; }), } : { key: "Record", disabled: !STATE.running, iconProps: { iconName: Icons.Record }, text: "Record", onClick: () => Recorder.start(), }); onClick: action(() => { RECORD_STATE.recorder.start(); RECORD_STATE.recording = true; }), } ); result.push({ key: "fullscreen", Loading apps/demo/src/components/scrcpy/recorder.ts +95 −50 Original line number Diff line number Diff line Loading @@ -4,7 +4,9 @@ import { splitH264Stream, } from "@yume-chan/scrcpy"; import { InspectStream } from "@yume-chan/stream-extra"; import { action, makeAutoObservable, reaction } from "mobx"; import WebMMuxer from "webm-muxer"; import { saveFile } from "../../utils"; // https://ffmpeg.org/doxygen/0.11/avc_8c-source.html#l00106 function h264ConfigurationToAvcDecoderConfigurationRecord( Loading Loading @@ -75,22 +77,14 @@ export class MuxerStream extends InspectStream<ScrcpyVideoStreamPacket> { } const sample = h264StreamToAvcSample(frame.data); this.muxer!.addVideoChunk( { byteLength: sample.byteLength, this.muxer!.addVideoChunkRaw( sample, frame.keyframe ? "key" : "delta", timestamp, type: frame.keyframe ? "key" : "delta", // Not used duration: null, copyTo: (destination) => { // destination is a Uint8Array (destination as Uint8Array).set(sample); }, }, { decoderConfig: this.configurationWritten this.configurationWritten ? undefined : { decoderConfig: { // Not used codec: "", description: this.avcConfiguration, Loading @@ -102,6 +96,7 @@ export class MuxerStream extends InspectStream<ScrcpyVideoStreamPacket> { constructor() { super((packet) => { try { if (packet.type === "configuration") { this.width = packet.data.croppedWidth; this.height = packet.data.croppedHeight; Loading @@ -126,6 +121,9 @@ export class MuxerStream extends InspectStream<ScrcpyVideoStreamPacket> { } this.appendFrame(packet); } catch (e) { console.error(e); } }); } Loading @@ -146,10 +144,6 @@ export class MuxerStream extends InspectStream<ScrcpyVideoStreamPacket> { this.appendFrame(frame); } } setTimeout(() => { this.stop(); }, 10000); } stop() { Loading @@ -158,12 +152,26 @@ export class MuxerStream extends InspectStream<ScrcpyVideoStreamPacket> { } const buffer = this.muxer.finalize()!; const blob = new Blob([buffer], { type: "video/webm" }); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = "scrcpy.webm"; a.click(); const now = new Date(); const stream = saveFile( // prettier-ignore `Recording ${ now.getFullYear() }-${ (now.getMonth() + 1).toString().padStart(2, '0') }-${ now.getDate().toString().padStart(2, '0') } ${ now.getHours().toString().padStart(2, '0') }-${ now.getMinutes().toString().padStart(2, '0') }-${ now.getSeconds().toString().padStart(2, '0') }.webm` ); const writer = stream.getWriter(); writer.write(new Uint8Array(buffer)); writer.close(); this.muxer = undefined; this.configurationWritten = false; Loading @@ -171,3 +179,40 @@ export class MuxerStream extends InspectStream<ScrcpyVideoStreamPacket> { this.firstTimestamp = -1; } } export const RECORD_STATE = makeAutoObservable({ recorder: new MuxerStream(), recording: false, intervalId: -1, hours: 0, minutes: 0, seconds: 0, }); reaction( () => RECORD_STATE.recording, (recording) => { if (recording) { RECORD_STATE.intervalId = window.setInterval( action(() => { RECORD_STATE.seconds += 1; if (RECORD_STATE.seconds >= 60) { RECORD_STATE.seconds = 0; RECORD_STATE.minutes += 1; } if (RECORD_STATE.minutes >= 60) { RECORD_STATE.minutes = 0; RECORD_STATE.hours += 1; } }), 1000 ); } else { window.clearInterval(RECORD_STATE.intervalId); RECORD_STATE.intervalId = -1; RECORD_STATE.hours = 0; RECORD_STATE.minutes = 0; RECORD_STATE.seconds = 0; } } ); apps/demo/src/components/scrcpy/state.tsx +8 −7 Original line number Diff line number Diff line Loading @@ -37,7 +37,7 @@ import { GlobalState } from "../../state"; import { Icons, ProgressStream } from "../../utils"; import { DeviceViewRef } from "../device-view"; import { fetchServer } from "./fetch-server"; import { MuxerStream } from "./recorder"; import { MuxerStream, RECORD_STATE } from "./recorder"; import { SettingDefinition, Settings } from "./settings"; export interface H264Decoder extends Disposable { Loading @@ -59,8 +59,6 @@ interface DecoderDefinition { Constructor: H264DecoderConstructor; } export const Recorder = new MuxerStream(); export class ScrcpyPageState { running = false; Loading Loading @@ -529,6 +527,7 @@ export class ScrcpyPageState { }) ); RECORD_STATE.recorder = new MuxerStream(); client.videoStream .pipeThrough( new InspectStream( Loading @@ -547,10 +546,7 @@ export class ScrcpyPageState { }) ) ) .pipeThrough(Recorder, { preventAbort: true, preventClose: true, }) .pipeThrough(RECORD_STATE.recorder) .pipeTo(decoder.writable) .catch(() => {}); Loading Loading @@ -603,6 +599,11 @@ export class ScrcpyPageState { this.decoder?.dispose(); this.decoder = undefined; if (RECORD_STATE.recording) { RECORD_STATE.recorder.stop(); RECORD_STATE.recording = false; } this.fps = 0; clearTimeout(this.fpsCounterIntervalId); Loading common/config/rush/pnpm-lock.yaml +14 −9 Original line number Diff line number Diff line Loading @@ -72,6 +72,7 @@ importers: react-dom: ^18.2.0 source-map-loader: ^4.0.1 typescript: ^4.9.4 webm-muxer: ^1.1.0 xterm: ^5.1.0 xterm-addon-fit: ^0.7.0 xterm-addon-search: ^0.11.0 Loading Loading @@ -103,6 +104,7 @@ importers: next: 13.1.1_biqbaboplfbrettd7655fr4n2y react: 18.2.0 react-dom: 18.2.0_react@18.2.0 webm-muxer: 1.1.0 xterm: 5.1.0 xterm-addon-fit: 0.7.0_xterm@5.1.0 xterm-addon-search: 0.11.0_xterm@5.1.0 Loading Loading @@ -2284,7 +2286,7 @@ packages: '@docusaurus/react-loadable': 5.5.2_react@18.2.0 '@docusaurus/types': 2.2.0_biqbaboplfbrettd7655fr4n2y '@types/history': 4.7.11 '@types/react': 17.0.27 '@types/react': 18.0.26 '@types/react-router-config': 5.0.6 '@types/react-router-dom': 5.3.3 react: 18.2.0 Loading Loading @@ -4182,14 +4184,6 @@ packages: '@types/react': 18.0.26 dev: false /@types/react/17.0.27: resolution: {integrity: sha512-zgiJwtsggVGtr53MndV7jfiUESTqrbxOcBvwfe6KS/9bzaVPCTDieTWnFNecVNx6EAaapg5xsLLWFfHHR437AA==} dependencies: '@types/prop-types': 15.7.5 '@types/scheduler': 0.16.2 csstype: 3.1.1 dev: false /@types/react/18.0.26: resolution: {integrity: sha512-hCR3PJQsAIXyxhTNSiDFY//LhnMZWpNNr5etoCqx/iUfGc5gXWtQR2Phl908jVR6uPXacojQWTg4qRpkxTuGug==} dependencies: Loading Loading @@ -4247,6 +4241,10 @@ packages: resolution: {integrity: sha512-cSjhgrr8g4KbPnnijAr/KJDNKa/bBa+ixYkywFRvrhvi9n1WEl7yYbtRyzE6jqNQiSxxJxoAW3STaOQwJHndaw==} dev: false /@types/wicg-file-system-access/2020.9.5: resolution: {integrity: sha512-UYK244awtmcUYQfs7FR8710MJcefL2WvkyHMjA8yJzxd1mo0Gfn88sRZ1Bls7hiUhA2w7ne1gpJ9T5g3G0wOyA==} dev: false /@types/ws/8.5.4: resolution: {integrity: sha512-zdQDHKUgcX/zBc4GrwsE/7dVdAD8JR4EuiAXiiUhhfyIJXXb2+PrGshFyeXWQPMmmZ2XxgaqclgpIC7eTXc1mg==} dependencies: Loading Loading @@ -12545,6 +12543,13 @@ packages: /webidl-conversions/3.0.1: resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} /webm-muxer/1.1.0: resolution: {integrity: sha512-r1xYFOKHkctRjG+Fj7D6B/2AW2aU6uL1rjvBcrj3CvgdL8qP6gB+l2Z81x+VG71vStbJJAyORfyq2blYKLr9sg==} dependencies: '@types/dom-webcodecs': 0.1.5 '@types/wicg-file-system-access': 2020.9.5 dev: false /webpack-bundle-analyzer/4.7.0: resolution: {integrity: sha512-j9b8ynpJS4K+zfO5GGwsAcQX4ZHpWV+yRiHDiL+bE0XHJ8NiPYLTNVQdlFYWxtpg9lfAQNlwJg16J9AJtFSXRg==} engines: {node: '>= 10.13.0'} Loading Loading
apps/demo/package.json +1 −0 Original line number Diff line number Diff line Loading @@ -36,6 +36,7 @@ "next": "13.1.1", "react": "^18.2.0", "react-dom": "^18.2.0", "webm-muxer": "^1.1.0", "xterm": "^5.1.0", "xterm-addon-fit": "^0.7.0", "xterm-addon-search": "^0.11.0", Loading
apps/demo/src/components/scrcpy/command-bar.tsx +34 −8 Original line number Diff line number Diff line Loading @@ -14,7 +14,8 @@ import { GlobalState } from "../../state"; import { Icons } from "../../utils"; import { ExternalLink } from "../external-link"; import { CommandBarSpacerItem } from "./command-bar-spacer-item"; import { Recorder, STATE } from "./state"; import { RECORD_STATE } from "./recorder"; import { STATE } from "./state"; const ITEMS = computed(() => { const result: ICommandBarItemProps[] = []; Loading @@ -36,13 +37,38 @@ const ITEMS = computed(() => { }); } result.push({ result.push( RECORD_STATE.recording ? { key: "Record", iconProps: { iconName: Icons.Record, style: { color: "red" }, }, // prettier-ignore text: `${ RECORD_STATE.hours ? `${RECORD_STATE.hours}:` : "" }${ RECORD_STATE.minutes.toString().padStart(2, "0") }:${ RECORD_STATE.seconds.toString().padStart(2, "0") }`, onClick: action(() => { RECORD_STATE.recorder.stop(); RECORD_STATE.recording = false; }), } : { key: "Record", disabled: !STATE.running, iconProps: { iconName: Icons.Record }, text: "Record", onClick: () => Recorder.start(), }); onClick: action(() => { RECORD_STATE.recorder.start(); RECORD_STATE.recording = true; }), } ); result.push({ key: "fullscreen", Loading
apps/demo/src/components/scrcpy/recorder.ts +95 −50 Original line number Diff line number Diff line Loading @@ -4,7 +4,9 @@ import { splitH264Stream, } from "@yume-chan/scrcpy"; import { InspectStream } from "@yume-chan/stream-extra"; import { action, makeAutoObservable, reaction } from "mobx"; import WebMMuxer from "webm-muxer"; import { saveFile } from "../../utils"; // https://ffmpeg.org/doxygen/0.11/avc_8c-source.html#l00106 function h264ConfigurationToAvcDecoderConfigurationRecord( Loading Loading @@ -75,22 +77,14 @@ export class MuxerStream extends InspectStream<ScrcpyVideoStreamPacket> { } const sample = h264StreamToAvcSample(frame.data); this.muxer!.addVideoChunk( { byteLength: sample.byteLength, this.muxer!.addVideoChunkRaw( sample, frame.keyframe ? "key" : "delta", timestamp, type: frame.keyframe ? "key" : "delta", // Not used duration: null, copyTo: (destination) => { // destination is a Uint8Array (destination as Uint8Array).set(sample); }, }, { decoderConfig: this.configurationWritten this.configurationWritten ? undefined : { decoderConfig: { // Not used codec: "", description: this.avcConfiguration, Loading @@ -102,6 +96,7 @@ export class MuxerStream extends InspectStream<ScrcpyVideoStreamPacket> { constructor() { super((packet) => { try { if (packet.type === "configuration") { this.width = packet.data.croppedWidth; this.height = packet.data.croppedHeight; Loading @@ -126,6 +121,9 @@ export class MuxerStream extends InspectStream<ScrcpyVideoStreamPacket> { } this.appendFrame(packet); } catch (e) { console.error(e); } }); } Loading @@ -146,10 +144,6 @@ export class MuxerStream extends InspectStream<ScrcpyVideoStreamPacket> { this.appendFrame(frame); } } setTimeout(() => { this.stop(); }, 10000); } stop() { Loading @@ -158,12 +152,26 @@ export class MuxerStream extends InspectStream<ScrcpyVideoStreamPacket> { } const buffer = this.muxer.finalize()!; const blob = new Blob([buffer], { type: "video/webm" }); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = "scrcpy.webm"; a.click(); const now = new Date(); const stream = saveFile( // prettier-ignore `Recording ${ now.getFullYear() }-${ (now.getMonth() + 1).toString().padStart(2, '0') }-${ now.getDate().toString().padStart(2, '0') } ${ now.getHours().toString().padStart(2, '0') }-${ now.getMinutes().toString().padStart(2, '0') }-${ now.getSeconds().toString().padStart(2, '0') }.webm` ); const writer = stream.getWriter(); writer.write(new Uint8Array(buffer)); writer.close(); this.muxer = undefined; this.configurationWritten = false; Loading @@ -171,3 +179,40 @@ export class MuxerStream extends InspectStream<ScrcpyVideoStreamPacket> { this.firstTimestamp = -1; } } export const RECORD_STATE = makeAutoObservable({ recorder: new MuxerStream(), recording: false, intervalId: -1, hours: 0, minutes: 0, seconds: 0, }); reaction( () => RECORD_STATE.recording, (recording) => { if (recording) { RECORD_STATE.intervalId = window.setInterval( action(() => { RECORD_STATE.seconds += 1; if (RECORD_STATE.seconds >= 60) { RECORD_STATE.seconds = 0; RECORD_STATE.minutes += 1; } if (RECORD_STATE.minutes >= 60) { RECORD_STATE.minutes = 0; RECORD_STATE.hours += 1; } }), 1000 ); } else { window.clearInterval(RECORD_STATE.intervalId); RECORD_STATE.intervalId = -1; RECORD_STATE.hours = 0; RECORD_STATE.minutes = 0; RECORD_STATE.seconds = 0; } } );
apps/demo/src/components/scrcpy/state.tsx +8 −7 Original line number Diff line number Diff line Loading @@ -37,7 +37,7 @@ import { GlobalState } from "../../state"; import { Icons, ProgressStream } from "../../utils"; import { DeviceViewRef } from "../device-view"; import { fetchServer } from "./fetch-server"; import { MuxerStream } from "./recorder"; import { MuxerStream, RECORD_STATE } from "./recorder"; import { SettingDefinition, Settings } from "./settings"; export interface H264Decoder extends Disposable { Loading @@ -59,8 +59,6 @@ interface DecoderDefinition { Constructor: H264DecoderConstructor; } export const Recorder = new MuxerStream(); export class ScrcpyPageState { running = false; Loading Loading @@ -529,6 +527,7 @@ export class ScrcpyPageState { }) ); RECORD_STATE.recorder = new MuxerStream(); client.videoStream .pipeThrough( new InspectStream( Loading @@ -547,10 +546,7 @@ export class ScrcpyPageState { }) ) ) .pipeThrough(Recorder, { preventAbort: true, preventClose: true, }) .pipeThrough(RECORD_STATE.recorder) .pipeTo(decoder.writable) .catch(() => {}); Loading Loading @@ -603,6 +599,11 @@ export class ScrcpyPageState { this.decoder?.dispose(); this.decoder = undefined; if (RECORD_STATE.recording) { RECORD_STATE.recorder.stop(); RECORD_STATE.recording = false; } this.fps = 0; clearTimeout(this.fpsCounterIntervalId); Loading
common/config/rush/pnpm-lock.yaml +14 −9 Original line number Diff line number Diff line Loading @@ -72,6 +72,7 @@ importers: react-dom: ^18.2.0 source-map-loader: ^4.0.1 typescript: ^4.9.4 webm-muxer: ^1.1.0 xterm: ^5.1.0 xterm-addon-fit: ^0.7.0 xterm-addon-search: ^0.11.0 Loading Loading @@ -103,6 +104,7 @@ importers: next: 13.1.1_biqbaboplfbrettd7655fr4n2y react: 18.2.0 react-dom: 18.2.0_react@18.2.0 webm-muxer: 1.1.0 xterm: 5.1.0 xterm-addon-fit: 0.7.0_xterm@5.1.0 xterm-addon-search: 0.11.0_xterm@5.1.0 Loading Loading @@ -2284,7 +2286,7 @@ packages: '@docusaurus/react-loadable': 5.5.2_react@18.2.0 '@docusaurus/types': 2.2.0_biqbaboplfbrettd7655fr4n2y '@types/history': 4.7.11 '@types/react': 17.0.27 '@types/react': 18.0.26 '@types/react-router-config': 5.0.6 '@types/react-router-dom': 5.3.3 react: 18.2.0 Loading Loading @@ -4182,14 +4184,6 @@ packages: '@types/react': 18.0.26 dev: false /@types/react/17.0.27: resolution: {integrity: sha512-zgiJwtsggVGtr53MndV7jfiUESTqrbxOcBvwfe6KS/9bzaVPCTDieTWnFNecVNx6EAaapg5xsLLWFfHHR437AA==} dependencies: '@types/prop-types': 15.7.5 '@types/scheduler': 0.16.2 csstype: 3.1.1 dev: false /@types/react/18.0.26: resolution: {integrity: sha512-hCR3PJQsAIXyxhTNSiDFY//LhnMZWpNNr5etoCqx/iUfGc5gXWtQR2Phl908jVR6uPXacojQWTg4qRpkxTuGug==} dependencies: Loading Loading @@ -4247,6 +4241,10 @@ packages: resolution: {integrity: sha512-cSjhgrr8g4KbPnnijAr/KJDNKa/bBa+ixYkywFRvrhvi9n1WEl7yYbtRyzE6jqNQiSxxJxoAW3STaOQwJHndaw==} dev: false /@types/wicg-file-system-access/2020.9.5: resolution: {integrity: sha512-UYK244awtmcUYQfs7FR8710MJcefL2WvkyHMjA8yJzxd1mo0Gfn88sRZ1Bls7hiUhA2w7ne1gpJ9T5g3G0wOyA==} dev: false /@types/ws/8.5.4: resolution: {integrity: sha512-zdQDHKUgcX/zBc4GrwsE/7dVdAD8JR4EuiAXiiUhhfyIJXXb2+PrGshFyeXWQPMmmZ2XxgaqclgpIC7eTXc1mg==} dependencies: Loading Loading @@ -12545,6 +12543,13 @@ packages: /webidl-conversions/3.0.1: resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} /webm-muxer/1.1.0: resolution: {integrity: sha512-r1xYFOKHkctRjG+Fj7D6B/2AW2aU6uL1rjvBcrj3CvgdL8qP6gB+l2Z81x+VG71vStbJJAyORfyq2blYKLr9sg==} dependencies: '@types/dom-webcodecs': 0.1.5 '@types/wicg-file-system-access': 2020.9.5 dev: false /webpack-bundle-analyzer/4.7.0: resolution: {integrity: sha512-j9b8ynpJS4K+zfO5GGwsAcQX4ZHpWV+yRiHDiL+bE0XHJ8NiPYLTNVQdlFYWxtpg9lfAQNlwJg16J9AJtFSXRg==} engines: {node: '>= 10.13.0'} Loading