feat: 🚀 添加音视频录制功能以及更多的高级选项

This commit is contained in:
viarotel 2023-10-12 17:35:27 +08:00
parent 7616242744
commit b6986d14de
25 changed files with 484 additions and 132 deletions

View File

@ -21,7 +21,7 @@ jobs:
- name: Install Node.js
uses: actions/setup-node@v3
with:
node-version: 16
node-version: 18
- name: Install Dependencies
run: npm install

View File

@ -1,4 +1,4 @@
name: release-please
name: release
on:
push:

View File

@ -81,10 +81,11 @@
1. 用户界面进行优化,制作合适的 Logo ✅
2. 内置的软件更新功能 ✅
3. 添加 macOS 及 linux 操作系统的支持 🚧
4. 添加外部控制栏 🚧
5. 支持语言国际化功能 🚧
6. 添加对游戏的增强功能 如游戏键位映射 🚧
3. 录制和保存音视频 ✅
4. 添加 macOS 及 linux 操作系统的支持 🚧
5. 添加外部控制栏 🚧
6. 支持语言国际化功能 🚧
7. 添加对游戏的增强功能 如游戏键位映射 🚧
## 常见问题

View File

@ -21,8 +21,12 @@
"@electron-toolkit/preload": "^2.0.0",
"@electron-toolkit/utils": "^2.0.0",
"@viarotel-org/design": "^0.7.0",
"dayjs": "^1.11.10",
"electron-updater": "^6.1.1",
"element-plus": "^2.3.14"
"element-plus": "^2.3.14",
"lodash-es": "^4.17.21",
"pinia": "^2.1.6",
"ufo": "^1.3.1"
},
"devDependencies": {
"@electron-toolkit/eslint-config": "^1.0.1",

View File

@ -17,12 +17,24 @@ dependencies:
'@viarotel-org/design':
specifier: ^0.7.0
version: 0.7.0
dayjs:
specifier: ^1.11.10
version: 1.11.10
electron-updater:
specifier: ^6.1.1
version: 6.1.4
element-plus:
specifier: ^2.3.14
version: 2.3.14(vue@3.3.4)
lodash-es:
specifier: ^4.17.21
version: 4.17.21
pinia:
specifier: ^2.1.6
version: 2.1.6(typescript@5.2.2)(vue@3.3.4)
ufo:
specifier: ^1.3.1
version: 1.3.1
devDependencies:
'@electron-toolkit/eslint-config':
@ -1595,6 +1607,10 @@ packages:
'@vue/compiler-dom': 3.3.4
'@vue/shared': 3.3.4
/@vue/devtools-api@6.5.1:
resolution: {integrity: sha512-+KpckaAQyfbvshdDW5xQylLni1asvNSGme1JFs8I1+/H5pHEhqUKMEQD/qn3Nx5+/nycBq11qAEi8lk+LXI2dA==}
dev: false
/@vue/eslint-config-prettier@8.0.0(eslint@8.49.0)(prettier@3.0.3):
resolution: {integrity: sha512-55dPqtC4PM/yBjhAr+yEw6+7KzzdkBuLmnhBrDfp4I48+wy+Giqqj9yUr5T2uD/BkBROjjmqnLZmXRdOx/VtQg==}
peerDependencies:
@ -2286,8 +2302,8 @@ packages:
- ts-node
dev: true
/dayjs@1.11.9:
resolution: {integrity: sha512-QvzAURSbQ0pKdIye2txOzNaHmxtUBXerpY0FJsFXUMKbIZeFm5ht1LS/jFsrncjnmtv8HsG0W2g6c0zUjZWmpA==}
/dayjs@1.11.10:
resolution: {integrity: sha512-vjAczensTgRcqDERK0SR2XMwsF/tSvnvlv6VcF2GIhg6Sx4yOIt/irsr1RDJsKiIyBzJDpCoXiWWq28MqH2cnQ==}
dev: false
/debug@3.2.7:
@ -2604,7 +2620,7 @@ packages:
'@types/lodash-es': 4.17.9
'@vueuse/core': 9.13.0(vue@3.3.4)
async-validator: 4.2.5
dayjs: 1.11.9
dayjs: 1.11.10
escape-html: 1.0.3
lodash: 4.17.21
lodash-es: 4.17.21
@ -4135,7 +4151,7 @@ packages:
acorn: 8.10.0
pathe: 1.1.1
pkg-types: 1.0.3
ufo: 1.3.0
ufo: 1.3.1
dev: true
/mrmime@1.0.1:
@ -4269,7 +4285,7 @@ packages:
dependencies:
destr: 2.0.1
node-fetch-native: 1.4.0
ufo: 1.3.0
ufo: 1.3.1
dev: true
/once@1.4.0:
@ -4447,6 +4463,24 @@ packages:
dev: true
optional: true
/pinia@2.1.6(typescript@5.2.2)(vue@3.3.4):
resolution: {integrity: sha512-bIU6QuE5qZviMmct5XwCesXelb5VavdOWKWaB17ggk++NUwQWWbP5YnsONTk3b752QkW9sACiR81rorpeOMSvQ==}
peerDependencies:
'@vue/composition-api': ^1.4.0
typescript: '>=4.4.4'
vue: ^2.6.14 || ^3.3.0
peerDependenciesMeta:
'@vue/composition-api':
optional: true
typescript:
optional: true
dependencies:
'@vue/devtools-api': 6.5.1
typescript: 5.2.2
vue: 3.3.4
vue-demi: 0.14.6(vue@3.3.4)
dev: false
/pirates@4.0.6:
resolution: {integrity: sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==}
engines: {node: '>= 6'}
@ -5193,11 +5227,9 @@ packages:
resolution: {integrity: sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==}
engines: {node: '>=14.17'}
hasBin: true
dev: true
/ufo@1.3.0:
resolution: {integrity: sha512-bRn3CsoojyNStCZe0BG0Mt4Nr/4KF+rhFlnNXybgqt5pXHNFRlqinSoQaTrGyzE4X8aHplSb+TorH+COin9Yxw==}
dev: true
/ufo@1.3.1:
resolution: {integrity: sha512-uY/99gMLIOlJPwATcMVYfqDSxUR9//AUcgZMzwfSTJPDKzA1S8mX4VLqa+fiAtveraQUBCz4FFcwVZBGbwBXIw==}
/unconfig@0.3.10:
resolution: {integrity: sha512-tj317lhIq2iZF/NXrJnU1t2UaGUKKz1eL1sK2t63Oq66V9BxqvZV12m55fp/fpQJ+DDmVlLgo7cnLVOZkhlO/A==}

View File

@ -1,10 +1,13 @@
import { join } from 'node:path'
import { BrowserWindow, app, shell } from 'electron'
import { electronApp, is, optimizer } from '@electron-toolkit/utils'
import iconPath from '../../resources/icons/icon.png?asset'
import winIconPath from '../../resources/icons/icon.ico?asset'
import macIconPath from '../../resources/icons/icon.icns?asset'
import ipcEvent from './ipc/index.js'
import ipcManage from './ipcManage/index.js'
function createWindow() {
let icon = iconPath
@ -46,7 +49,7 @@ function createWindow() {
mainWindow.loadFile(join(__dirname, '../renderer/index.html'))
}
ipcEvent(mainWindow)
ipcManage(mainWindow)
}
// This method will be called when Electron has finished

View File

@ -0,0 +1,41 @@
import { dialog, ipcMain, shell } from 'electron'
export default () => {
ipcMain.handle('show-open-dialog', async (event, params) => {
// console.log('params', params)
try {
const res = await dialog.showOpenDialog(params)
// console.log('showOpenDialog.res', res)
if (res.canceled) {
return false
}
return res.filePaths
}
catch (error) {
console.warn(error?.message || error)
return false
}
})
ipcMain.handle('open-path', async (event, pathValue) => {
try {
await shell.openPath(pathValue)
return true
}
catch (error) {
console.warn(error?.message || error)
return false
}
})
ipcMain.handle('show-item-in-folder', async (event, filePath) => {
try {
await shell.showItemInFolder(filePath)
return true
}
catch (error) {
console.warn(error?.message || error)
return false
}
})
}

View File

@ -1,10 +1,14 @@
import { app, ipcMain } from 'electron'
import updaterEvents from './updater/index.js'
import updater from './updater/index.js'
import handles from './handles/index.js'
export default (mainWindow) => {
handles(mainWindow)
updater(mainWindow)
ipcMain.on('restart-app', () => {
app.relaunch()
app.quit()
})
updaterEvents(mainWindow)
}

View File

@ -1,9 +1,8 @@
import path from 'node:path'
import { app, ipcMain } from 'electron'
import { is } from '@electron-toolkit/utils'
import { autoUpdater } from 'electron-updater'
const path = require('node:path')
export default (mainWindow) => {
// dev-start, 这里是为了在本地做应用升级测试使用,正式环境请务必删除
if (is.dev && process.env.ELECTRON_RENDERER_URL) {

View File

@ -1,8 +1,9 @@
import util from 'node:util'
import child_process from 'node:child_process'
import { Adb } from '@devicefarmer/adbkit'
import adbPath from '@resources/core/adb.exe?asset&asarUnpack'
const util = require('node:util')
const exec = util.promisify(require('node:child_process').exec)
const exec = util.promisify(child_process.exec)
let client = null

View File

@ -1,9 +1,12 @@
import path from 'node:path'
import electron from './electron/index.js'
import adbkit from './adbkit/index.js'
import scrcpy from './scrcpy/index.js'
export default {
install(expose) {
expose('nodePath', path)
expose('electron', electron())
expose('adbkit', adbkit())
expose('scrcpy', scrcpy())

View File

@ -1,8 +1,9 @@
import util from 'node:util'
import child_process from 'node:child_process'
import adbPath from '@resources/core/adb.exe?asset&asarUnpack'
import scrcpyPath from '@resources/core/scrcpy.exe?asset&asarUnpack'
const util = require('node:util')
const exec = util.promisify(require('node:child_process').exec)
const exec = util.promisify(child_process.exec)
const shell = command =>
exec(`${scrcpyPath} ${command}`, { env: { ...process.env, ADB: adbPath } })

View File

@ -1,6 +1,6 @@
<template>
<div class="absolute inset-0 px-4 pb-4 h-full overflow-hidden">
<el-tabs v-model="activeTab" class="el-tabs-flex" @tab-click="handleClick">
<el-tabs v-model="activeTab" class="el-tabs-flex" @tab-change="onTabChange">
<el-tab-pane
v-for="(item, index) of tabsModel"
:key="index"
@ -8,7 +8,14 @@
:name="item.prop"
lazy
>
<component :is="item.prop" ref="component" />
<component
:is="item.prop"
:ref="item.prop"
:scrcpy-cache="scrcpyCache"
:get-scrcpy-cache="getScrcpyCache"
:set-scrcpy-cache="setScrcpyCache"
:get-scrcpy-map="getScrcpyMap"
/>
</el-tab-pane>
</el-tabs>
</div>
@ -44,8 +51,12 @@ export default {
activeTab: 'Devices',
}
},
mounted() {},
methods: {},
created() {
this.$store.scrcpy.init()
},
methods: {
onTabChange(prop) {},
},
}
</script>

View File

@ -8,7 +8,7 @@
<a class="hover:underline text-primary-500" :href="escrcpyURL" target="_blank">Scrcpy</a>
显示和控制您的 Android 设备 Electron 驱动
</div>
<div class="pt-16 pb-4">
<div class="pt-12 pb-4">
<el-button :loading="loading" type="primary" size="large" @click="handleUpdate">
{{ loading && percent ? `正在更新中...${percent.toFixed(1)}%` : '版本检测更新' }}
</el-button>
@ -41,6 +41,10 @@ export default {
this.onUpdateError()
},
methods: {
handleUpdate() {
this.loading = true
this.$electron.ipcRenderer.send('check-for-update')
},
onUpdateNotAvailable() {
this.$electron.ipcRenderer.on('update-not-available', () => {
this.loading = false
@ -69,10 +73,6 @@ export default {
}
})
},
handleUpdate() {
this.loading = true
this.$electron.ipcRenderer.send('check-for-update')
},
onDownloadProgress() {
this.$electron.ipcRenderer.on('download-progress', async (event, ret) => {
console.log('ret', ret)

View File

@ -14,21 +14,42 @@
</div>
</template>
<div class="">
<el-form ref="elForm" :model="scrcpyForm" label-width="120px" class="pr-8 pt-4">
<el-form ref="elForm" :model="scrcpyForm" label-width="125px" class="pr-8 pt-4">
<el-row :gutter="20">
<el-col
v-for="(item_1, index_1) of getScrcpyConfig(item.type)"
v-for="(item_1, index_1) of getSubModel(item.type)"
:key="index_1"
:span="12"
:offset="0"
>
<el-form-item :label="item_1.label" :prop="item_1.field">
<template #label>
<div class="flex items-center">
<el-tooltip
v-if="item_1.tips"
class=""
effect="dark"
:content="item_1.tips"
placement="bottom"
>
<el-link
class="mr-1 !text-base"
icon="InfoFilled"
type="warning"
:underline="false"
>
</el-link>
</el-tooltip>
<span class="">{{ item_1.label }}</span>
</div>
</template>
<el-input
v-if="item_1.type === 'input'"
v-bind="item_1.props || {}"
v-model="scrcpyForm[item_1.field]"
class="!w-full"
:placeholder="item_1.placeholder"
clearable
></el-input>
<el-input
v-if="item_1.type === 'input.number'"
@ -36,6 +57,17 @@
v-model.number="scrcpyForm[item_1.field]"
class="!w-full"
:placeholder="item_1.placeholder"
clearable
></el-input>
<el-input
v-if="item_1.type === 'input.directory'"
v-bind="item_1.props || {}"
:value="scrcpyForm[item_1.field]"
readonly
class="!w-full"
clearable
:placeholder="item_1.placeholder"
@click="handleDirectory(item_1)"
></el-input>
<el-switch
v-if="item_1.type === 'switch'"
@ -43,6 +75,7 @@
v-model="scrcpyForm[item_1.field]"
class="!w-full"
:title="item_1.placeholder"
clearable
></el-switch>
<el-select
v-if="item_1.type === 'select'"
@ -50,6 +83,7 @@
v-model="scrcpyForm[item_1.field]"
:placeholder="item_1.placeholder"
class="!w-full"
clearable
>
<el-option
v-for="(item_2, index_2) in item_1.options"
@ -69,14 +103,12 @@
</template>
<script>
import storage from '@renderer/utils/storages'
import * as scrcpyConfigs from './configs/index.js'
import { debounce } from 'lodash-es'
import { useScrcpyStore } from '@renderer/store/index.js'
export default {
emits: ['change'],
data() {
const scrcpyCache = storage.get('scrcpyCache') || {}
// console.log('scrcpyCache', scrcpyCache)
const scrcpyStore = useScrcpyStore()
return {
scrcpyModel: [
@ -88,54 +120,58 @@ export default {
label: '设备控制',
type: 'device',
},
{
label: '音频控制',
type: 'audio',
},
{
label: '窗口控制',
type: 'window',
},
{
label: '录制音视频',
type: 'record',
},
{
label: '音频控制',
type: 'audio',
},
],
scrcpyForm: { ...this.getDefaultValues(), ...scrcpyCache },
scrcpyForm: { ...scrcpyStore.config },
}
},
watch: {
scrcpyForm: {
handler() {
storage.set('scrcpyCache', this.scrcpyForm)
this.$message.success('保存配置成功,将在下一次控制设备时生效')
this.handleSave()
},
deep: true,
},
},
methods: {
getScrcpyConfig(type) {
const value = scrcpyConfigs[type]()
return value
created() {
this.handleSave = debounce(this.handleSave, 1000, { leading: false, trailing: true })
},
getDefaultValues(type) {
const model = []
if (type) {
model.push(...this.getScrcpyConfig(type))
}
else {
// console.log('scrcpyConfigs', scrcpyConfigs)
const values = Object.values(scrcpyConfigs)
model.push(...values.flatMap(handler => handler()))
methods: {
async handleDirectory({ field }) {
const res = await this.$electron.ipcRenderer.invoke('show-open-dialog', {
properties: ['openDirectory'],
})
if (!res) {
return false
}
const value = model.reduce((obj, item) => {
const { field, value } = item
obj[field] = value
return obj
}, {})
const value = this.$path.normalize(res[0])
this.scrcpyForm[field] = value
},
handleSave() {
this.$store.scrcpy.updateConfig(this.scrcpyForm)
this.$message.success('保存配置成功,将在下一次控制设备时生效')
},
getSubModel(type) {
const value = this.$store.scrcpy.getModel(type)
return value
},
handleReset(type) {
this.scrcpyForm = { ...this.scrcpyForm, ...this.getDefaultValues(type) }
storage.set('scrcpyCache', this.scrcpyForm)
this.scrcpyForm = { ...this.scrcpyForm, ...this.$store.scrcpy.getDefaultConfig(type) }
this.$store.scrcpy.updateConfig(this.scrcpyForm)
},
},
}

View File

@ -41,8 +41,8 @@
<template #empty>
<el-empty description="设备列表为空" />
</template>
<el-table-column prop="id" label="设备 ID" show-overflow-tooltip />
<el-table-column prop="name" label="设备名称" show-overflow-tooltip>
<el-table-column prop="id" label="设备 ID" show-overflow-tooltip align="left" />
<el-table-column prop="name" label="设备名称" show-overflow-tooltip align="left">
<template #default="{ row }">
<div class="flex items-center">
<el-tooltip
@ -63,7 +63,7 @@
</div>
</template>
</el-table-column>
<el-table-column label="操作" width="350" align="left">
<el-table-column label="操作" width="450" align="left">
<template #default="{ row }">
<el-button
type="primary"
@ -73,6 +73,14 @@
>
{{ row.$loading ? '镜像中' : '开始镜像' }}
</el-button>
<el-button
type="primary"
:loading="row.$recordLoading"
:disabled="row.$unauthorized"
@click="handleRecord(row)"
>
{{ row.$recordLoading ? '录制中' : '开始录制' }}
</el-button>
<el-button
v-if="!row.$wireless"
type="primary"
@ -104,6 +112,7 @@
<script>
import { isIPWithPort, sleep } from '@renderer/utils/index.js'
import storage from '@renderer/utils/storages'
import dayjs from 'dayjs'
import PairDialog from './PairDialog/index.vue'
export default {
@ -123,8 +132,17 @@ export default {
},
}
},
computed: {
scrcpyConfig() {
return this.$store.scrcpy.config
},
stringScrcpyConfig() {
return this.$store.scrcpy.stringConfig
},
},
created() {
this.getDeviceData()
this.$adb.watch(async (type, ret) => {
console.log('adb.watch.ret', ret)
@ -139,6 +157,58 @@ export default {
})
},
methods: {
getRecordPath(row) {
const defaultPath = this.$path.resolve('../')
// console.log('defaultPath', defaultPath)
const basePath = this.scrcpyConfig['--record'] || defaultPath
const recordFormat = this.scrcpyConfig['--record-format'] || 'mp4'
const fileName = `${row.name || row.id}-${dayjs().format(
'YYYY-MM-DD-HH-mm-ss',
)}.${recordFormat}`
const joinValue = this.$path.join(basePath, fileName)
const value = this.$path.normalize(joinValue)
return value
},
async handleRecord(row) {
row.$recordLoading = true
const recordPath = this.getRecordPath(row)
try {
const command = `--serial=${row.id} --window-title=${row.name}-${row.id} --record=${recordPath} ${this.stringScrcpyConfig}`
console.log('handleRecord.command', command)
await this.$scrcpy.shell(command)
await this.$confirm('是否前往录制位置进行查看?', '录制成功', {
confirmButtonText: '确定',
cancelButtonText: '取消',
closeOnClickModal: false,
type: 'success',
})
this.$electron.ipcRenderer.invoke('show-item-in-folder', recordPath)
}
catch (error) {
if (error.message) {
this.$message.warning(error.message)
}
}
row.$recordLoading = false
},
async handleMirror(row) {
row.$loading = true
try {
await this.$scrcpy.shell(
`--serial=${row.id} --window-title=${row.name}-${row.id} ${this.stringScrcpyConfig}`,
)
}
catch (error) {
if (error.message) {
this.$message.warning(error.message)
}
}
row.$loading = false
},
async handleWifi(row) {
try {
const host = await this.$adb.getDeviceIP(row.id)
@ -193,6 +263,7 @@ export default {
'连接设备失败',
{
dangerouslyUseHTMLString: true,
closeOnClickModal: false,
confirmButtonText: '无线配对',
cancelButtonText: '取消',
type: 'warning',
@ -218,41 +289,7 @@ export default {
}
row.$stopLoading = false
},
async handleMirror(row) {
row.$loading = true
try {
await this.$scrcpy.shell(
`--serial=${row.id} --window-title=${row.name}-${row.id} ${this.addScrcpyConfigs()}`,
)
}
catch (error) {
if (error.message) {
this.$message.warning(error.message)
}
}
row.$loading = false
},
addScrcpyConfigs() {
const configs = storage.get('scrcpyCache') || {}
const value = Object.entries(configs)
.reduce((arr, [key, value]) => {
if (!value) {
return arr
}
if (typeof value === 'boolean') {
arr.push(key)
}
else {
arr.push(`${key}=${value}`)
}
return arr
}, [])
.join(' ')
console.log('addScrcpyConfigs.value', value)
return value
},
async getDeviceData() {
this.loading = true
await sleep()
@ -262,6 +299,7 @@ export default {
...item,
name: item.model ? item.model.split(':')[1] : '未授权设备',
$loading: false,
$recordLoading: false,
$stopLoading: false,
$unauthorized: item.type === 'unauthorized',
$wireless: isIPWithPort(item.id),

View File

@ -1,6 +1,8 @@
import { createApp } from 'vue'
import App from './App.vue'
import store from './store/index.js'
import plugins from './plugins/index.js'
import 'virtual:uno.css'
@ -8,10 +10,13 @@ import './styles/index.js'
const app = createApp(App)
app.use(store)
app.use(plugins)
app.config.globalProperties.$electron = window.electron
app.config.globalProperties.$adb = window.adbkit
app.config.globalProperties.$scrcpy = window.scrcpy
app.config.globalProperties.$path = window.nodePath
app.mount('#app')

View File

@ -0,0 +1,15 @@
import { createPinia } from 'pinia'
import { useScrcpyStore } from './scrcpy/index.js'
export { useScrcpyStore }
export default {
install(app) {
const store = createPinia()
app.use(store)
app.config.globalProperties.$store = {
scrcpy: useScrcpyStore(),
}
},
}

View File

@ -0,0 +1,80 @@
import { defineStore } from 'pinia'
import storage from '@renderer/utils/storages'
import { cloneDeep } from 'lodash-es'
import * as model from './model/index.js'
export const useScrcpyStore = defineStore({
id: 'app-scrcpy',
state() {
return {
model,
config: storage.get('scrcpyConfig'),
excludeKeys: ['--record', '--record-format'],
}
},
getters: {
stringConfig() {
if (!this.config) {
return ''
}
const value = Object.entries(this.config)
.reduce((arr, [key, value]) => {
if (!value) {
return arr
}
if (this.excludeKeys.includes(key)) {
return arr
}
if (typeof value === 'boolean') {
arr.push(key)
}
else {
arr.push(`${key}=${value}`)
}
return arr
}, [])
.join(' ')
console.log('stringifyConfig.value', value)
return value
},
},
actions: {
init() {
this.config = this.config || this.getDefaultConfig()
return this.config
},
updateConfig(value) {
this.config = cloneDeep(value)
storage.set('scrcpyConfig', this.config)
},
getModel(key, params) {
const handler = this.model[key]
return handler(params)
},
getDefaultConfig(type) {
const model = []
if (type) {
const handler = this.model[type]
model.push(...handler())
}
else {
// console.log('scrcpyModel', scrcpyModel)
const values = Object.values(this.model)
model.push(...values.flatMap(handler => handler()))
}
const value = model.reduce((obj, item) => {
const { field, value } = item
obj[field] = value
return obj
}, {})
return value
},
},
})

View File

@ -5,14 +5,16 @@ export default () => {
type: 'switch',
field: '--show-touches',
value: false,
placeholder: '开启后将打开开发者选项中的显示点按触摸反馈(仅在物理设备上展示)',
placeholder: '开启后将打开开发者选项中的显示点按触摸反馈',
tips: '仅在物理设备上展示',
},
{
label: '保持清醒',
type: 'switch',
field: '--stay-awake',
value: false,
placeholder: '开启以防止设备进入睡眠状态(仅有线方式连接时有效)',
placeholder: '开启以防止设备进入睡眠状态',
tips: '仅有线方式连接时有效',
},
{
label: '关闭屏幕',

View File

@ -2,3 +2,4 @@ export { default as video } from './video/index.js'
export { default as device } from './device/index.js'
export { default as window } from './window/index.js'
export { default as audio } from './audio/index.js'
export { default as record } from './record/index.js'

View File

@ -0,0 +1,28 @@
export default () => {
return [
{
label: '录制存储路径',
type: 'input.directory',
field: '--record',
value: '',
placeholder: '默认值为执行应用的同级目录',
},
{
label: '录制视频格式',
type: 'select',
field: '--record-format',
value: 'mp4',
placeholder: '默认值为 mp4',
options: [
{
label: 'mp4',
value: 'mp4',
},
{
label: 'mkv',
value: 'mkv',
},
],
},
]
}

View File

@ -22,24 +22,11 @@ export default () => {
placeholder: '默认值为 60',
},
{
label: '屏幕旋转',
type: 'select',
field: '--rotation',
value: '',
placeholder: '默认值为设备屏幕旋转角度',
options: [
{ label: '0°', value: '0' },
{ label: '-90°', value: '1' },
{ label: '180°', value: '2' },
{ label: '90°', value: '3' },
],
},
{
label: '解码器',
label: '视频解码器',
type: 'select',
field: '--video-codec',
value: '',
placeholder: '解码器默认值为 h264',
placeholder: '默认值为 h264',
options: [
{
label: 'h264',
@ -56,11 +43,11 @@ export default () => {
],
},
{
label: '编码器',
label: '视频编码器',
type: 'select',
field: '--video-encoder',
value: '',
placeholder: '编码器默认值为 h264',
placeholder: '默认值为设备默认编码器',
// "[server] INFO: List of video encoders:"
// "--video-codec=h264 --video-encoder='OMX.qcom.video.encoder.avc'"
// "--video-codec=h264 --video-encoder='c2.android.avc.encoder'"
@ -69,26 +56,86 @@ export default () => {
// "--video-codec=h265 --video-encoder='c2.android.hevc.encoder'"
options: [
{
label: 'OMX.qcom.video.encoder.avc',
label: 'Qualcomm AVC(H.264) 视频编码器',
value: 'OMX.qcom.video.encoder.avc',
},
{
label: 'c2.android.avc.encoder',
label: 'Android AVC(H.264) 视频编码器',
value: 'c2.android.avc.encoder',
},
{
label: 'OMX.google.h264.encoder',
label: 'Google H.264(AVC) 视频编码器',
value: 'OMX.google.h264.encoder',
},
{
label: 'OMX.qcom.video.encoder.hevc',
label: 'Qualcomm HEVC(H.265) 视频编码器',
value: 'OMX.qcom.video.encoder.hevc',
},
{
label: 'c2.android.hevc.encoder',
label: 'Android HEVC(H.265) 视频编码器',
value: 'c2.android.hevc.encoder',
},
],
},
{
label: '屏幕旋转',
type: 'select',
field: '--rotation',
value: '',
placeholder: '默认值为设备屏幕旋转角度',
options: [
{ label: '0°', value: '0' },
{ label: '-90°', value: '1' },
{ label: '180°', value: '2' },
{ label: '90°', value: '3' },
],
},
{
label: '屏幕裁剪',
type: 'input',
field: '--crop',
value: '',
placeholder: '默认不裁剪,格式为 1224:1440:0:0',
},
{
label: '多显示器',
type: 'select',
field: '--display',
value: '',
placeholder: '默认值为 0主屏幕',
options: [
{ label: '0', value: '0' },
{ label: '1', value: '1' },
{ label: '2', value: '2' },
],
},
{
label: '视频缓冲',
type: 'input.number',
field: '--display-buffer',
value: '',
placeholder: '单位为 ms默认值为 0ms',
},
{
label: '音频缓冲',
type: 'input.number',
field: '--audio-buffer',
value: '',
placeholder: '单位为 ms默认值为 0ms',
},
{
label: '接收器(v4l2)缓冲',
type: 'input.number',
field: '--v4l2-buffer',
value: '',
placeholder: '单位为 ms默认值为 0ms',
},
{
label: '禁用视频',
type: 'switch',
field: '--no-video',
value: false,
placeholder: '开启后将禁用视频',
},
]
}