Taobin-Recipe-Manager/client-electron/src/pages/android/components/scrcpy-tab.tsx
2024-02-05 17:10:34 +07:00

322 lines
10 KiB
TypeScript

import { Button } from '@/components/ui/button'
import { Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter } from '@/components/ui/card'
import { toast } from '@/components/ui/use-toast'
import useAdb from '@/hooks/useAdb'
import { type AdbSubprocessProtocol } from '@yume-chan/adb'
import { AdbScrcpyClient, AdbScrcpyOptions1_22 } from '@yume-chan/adb-scrcpy'
import {
AndroidKeyCode,
AndroidKeyEventAction,
AndroidMotionEventAction,
ScrcpyLogLevel1_18,
ScrcpyOptions1_25,
ScrcpyPointerId,
ScrcpyVideoCodecId
} from '@yume-chan/scrcpy'
import { WebCodecsDecoder } from '@yume-chan/scrcpy-decoder-webcodecs'
import { Consumable, WritableStream, ReadableStream, DecodeUtf8Stream } from '@yume-chan/stream-extra'
import { useCallback, useEffect, useRef, useState } from 'react'
import { Terminal } from 'xterm'
import { FitAddon } from 'xterm-addon-fit'
import 'xterm/css/xterm.css'
export const ScrcpyTab: React.FC = () => {
const adb = useAdb(state => state.adb)
const logcatRef = useRef<HTMLDivElement>(null)
const scrcpyScreenRef = useRef<HTMLDivElement>(null)
const [process, setProcess] = useState<AdbSubprocessProtocol | undefined>()
const [client, setClient] = useState<AdbScrcpyClient | undefined>()
const [decoder, setDecoder] = useState<WebCodecsDecoder | undefined>()
useEffect(() => {
const startTerminal = async () => {
if (logcatRef.current && adb) {
if (logcatRef.current.children.length > 0) {
// remove all children from the logcatRef
while (logcatRef.current.firstChild) {
logcatRef.current.removeChild(logcatRef.current.firstChild)
}
}
const terminal: Terminal = new Terminal()
const fitAddon = new FitAddon()
terminal.loadAddon(fitAddon)
const process: AdbSubprocessProtocol = await adb.subprocess.shell('logcat')
process.stdout.pipeTo(
new WritableStream<Uint8Array>({
write(chunk) {
terminal.write(chunk)
}
})
)
terminal.options.disableStdin = true
terminal.options.theme = {
background: '#1e1e1e',
foreground: '#d4d4d4'
}
terminal.open(logcatRef.current)
fitAddon.fit()
setProcess(process)
}
}
startTerminal()
return () => {
logcatRef.current && logcatRef.current.firstChild && logcatRef.current.removeChild(logcatRef.current.firstChild)
process?.stderr.cancel()
process?.stdout.cancel()
process?.kill()
}
}, [logcatRef, adb])
const connectScrcpy = useCallback(async () => {
if (!adb) {
toast({
title: 'No ADB connection',
description: 'Please connect to a device first',
duration: 3000,
variant: 'destructive'
})
return
}
// clean up the scrcpy screen
if (scrcpyScreenRef.current && scrcpyScreenRef.current.children.length > 0) {
while (scrcpyScreenRef.current.firstChild) {
scrcpyScreenRef.current.removeChild(scrcpyScreenRef.current.firstChild)
}
}
// fetch the scrcpy server binary
// TODO: should load from real server instead of local file. Fix this later
const server: ArrayBuffer = await fetch(new URL('../../../scrcpy/scrcpy_server_v1.25', import.meta.url)).then(res =>
res.arrayBuffer()
)
// push the server binary to the device
const sync = await adb.sync()
try {
await sync.write({
filename: '/data/local/tmp/scrcpy-server.jar',
file: new ReadableStream({
start(controller) {
controller.enqueue(new Consumable(new Uint8Array(server)))
controller.close()
}
})
})
} finally {
await sync.dispose()
}
// start the scrcpy server
const res = await adb.subprocess.spawn(
'CLASSPATH=/data/local/tmp/scrcpy-server.jar app_process / com.genymobile.scrcpy.Server 1.25'
)
// pipe the server output to the console
res.stdout.pipeThrough(new DecodeUtf8Stream()).pipeTo(
new WritableStream({
write(chunk) {
console.log(chunk)
}
})
)
const scrcpyOption = new ScrcpyOptions1_25({
maxFps: 60,
bitRate: 4000000,
stayAwake: true,
control: true,
logLevel: ScrcpyLogLevel1_18.Debug
})
// start the scrcpy client
const _client = await AdbScrcpyClient.start(
adb,
'/data/local/tmp/scrcpy-server.jar',
'1.25',
new AdbScrcpyOptions1_22(scrcpyOption)
)
// get the video stream
const videoStream = await _client?.videoStream
// create a decoder
const _decoder = new WebCodecsDecoder(ScrcpyVideoCodecId.H264)
scrcpyScreenRef.current?.appendChild(_decoder.renderer)
_decoder.renderer.style.width = '100%'
_decoder.renderer.style.height = '100%'
// pipe the video stream to the decoder
videoStream?.stream.pipeTo(_decoder.writable)
// if client has controlMessageWriter, Inject mouse and button events
if (_client.controlMessageWriter) {
_decoder.renderer.addEventListener('mousedown', e => {
// client width and height 450 x 800
const react = _decoder.renderer.getBoundingClientRect()
// normalize to _decoder.renderer.width and height 1080 x 1920
const x = ((e.clientX - react.left) * _decoder.renderer.width) / react.width
const y = ((e.clientY - react.top) * _decoder.renderer.height) / react.height
//console.log('mouse down at ' + x + ' ' + y)
_client.controlMessageWriter?.injectTouch({
action: AndroidMotionEventAction.Down,
pointerId: ScrcpyPointerId.Mouse | ScrcpyPointerId.Finger,
pointerX: x,
pointerY: y,
pressure: 1,
screenWidth: _decoder.renderer.width,
screenHeight: _decoder.renderer.height,
buttons: 0,
actionButton: 0
})
})
_decoder.renderer.addEventListener('mouseup', e => {
// client width and height 450 x 800
const react = _decoder.renderer.getBoundingClientRect()
// normalize to _decoder.renderer.width and height 1080 x 1920
const x = ((e.clientX - react.left) * _decoder.renderer.width) / react.width
const y = ((e.clientY - react.top) * _decoder.renderer.height) / react.height
//console.log('mouse up at ' + x + ' ' + y)
_client.controlMessageWriter?.injectTouch({
action: AndroidMotionEventAction.Up,
pointerId: ScrcpyPointerId.Mouse,
pointerX: x,
pointerY: y,
pressure: 1,
screenWidth: _decoder.renderer.width,
screenHeight: _decoder.renderer.height,
buttons: 0,
actionButton: 0
})
})
_decoder.renderer.addEventListener('mousemove', e => {
// client width and height 450 x 800
const react = _decoder.renderer.getBoundingClientRect()
// normalize to _decoder.renderer.width and height 1080 x 1920
const x = ((e.clientX - react.left) * _decoder.renderer.width) / react.width
const y = ((e.clientY - react.top) * _decoder.renderer.height) / react.height
//console.log('mouse move at ' + x + ' ' + y)
_client.controlMessageWriter?.injectTouch({
action: AndroidMotionEventAction.Move,
pointerId: ScrcpyPointerId.Mouse,
pointerX: x,
pointerY: y,
pressure: 1,
screenWidth: _decoder.renderer.width,
screenHeight: _decoder.renderer.height,
buttons: 0,
actionButton: 0
})
})
}
setDecoder(_decoder)
setClient(_client)
}, [adb, scrcpyScreenRef])
function onHomeClickHandler() {
client?.controlMessageWriter?.injectKeyCode({
action: AndroidKeyEventAction.Up,
keyCode: AndroidKeyCode.AndroidHome,
metaState: 0,
repeat: 0
})
}
function onBackClickHandler() {
client?.controlMessageWriter?.injectKeyCode({
action: AndroidKeyEventAction.Up,
keyCode: AndroidKeyCode.AndroidBack,
metaState: 0,
repeat: 0
})
}
function disconnectScrcpy() {
// clean ref
if (scrcpyScreenRef.current && scrcpyScreenRef.current.children.length > 0) {
while (scrcpyScreenRef.current.firstChild) {
scrcpyScreenRef.current.removeChild(scrcpyScreenRef.current.firstChild)
}
}
decoder?.dispose()
client?.close()
setClient(undefined)
setDecoder(undefined)
}
return (
<Card>
<CardHeader>
<CardTitle>Scrcpy</CardTitle>
<CardDescription>Stream and control your Android device from your computer</CardDescription>
</CardHeader>
<CardContent className="flex w-full justify-around items-start">
<div>
<div className="w-[450px] max-w-[450px] h-[800px] max-h-[800px] bg-slate-700" ref={scrcpyScreenRef} />
<div className="flex pt-3 justify-center items-center space-x-4 w-[450px]">
<Button onClick={onHomeClickHandler} variant={'outline'} className="flex-1">
Home
</Button>
<Button onClick={onBackClickHandler} variant={'outline'} className="flex-1">
Back
</Button>
</div>
</div>
<div className="flex flex-col space-y-4 w-full px-5">
<Card>
<CardHeader>
<CardTitle>Control</CardTitle>
</CardHeader>
<CardContent>
<div className="flex space-x-4 items-center">
{client ? (
<Button onClick={disconnectScrcpy} variant="destructive">
Disconnect
</Button>
) : (
<Button onClick={connectScrcpy} variant="default">
Connect
</Button>
)}
</div>
</CardContent>
</Card>
{/* logcat card */}
<Card>
<CardHeader>
<CardTitle>Logcat</CardTitle>
</CardHeader>
<CardContent>
<div className="flex space-x-4 items-center">
<div className="w-full h-96 bg-slate-700" ref={logcatRef} />
</div>
</CardContent>
</Card>
</div>
</CardContent>
<CardFooter>
<Button>Save changes</Button>
</CardFooter>
</Card>
)
}