feat: 📸 Enhanced recording

This commit is contained in:
viarotel 2024-10-28 15:55:49 +08:00
parent 9555f58df5
commit 7f10161ad7
24 changed files with 291 additions and 216 deletions

91
.vscode/settings.json vendored
View File

@ -1,15 +1,67 @@
{
"eslint.codeAction.showDocumentation": {
"enable": true
},
"eslint.format.enable": true,
"prettier.enable": false,
"editor.formatOnSave": false,
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit",
"source.organizeImports": "never"
},
"eslint.format.enable": true,
"eslint.codeAction.showDocumentation": {
"enable": true
},
"eslint.rules.customizations": [
{
"rule": "style/*",
"severity": "off",
"fixable": true
},
{
"rule": "format/*",
"severity": "off",
"fixable": true
},
{
"rule": "*-indent",
"severity": "off",
"fixable": true
},
{
"rule": "*-spacing",
"severity": "off",
"fixable": true
},
{
"rule": "*-spaces",
"severity": "off",
"fixable": true
},
{
"rule": "*-order",
"severity": "off",
"fixable": true
},
{
"rule": "*-dangle",
"severity": "off",
"fixable": true
},
{
"rule": "*-newline",
"severity": "off",
"fixable": true
},
{
"rule": "*quotes",
"severity": "off",
"fixable": true
},
{
"rule": "*semi",
"severity": "off",
"fixable": true
}
],
"eslint.validate": [
// "jsonc",
"javascript",
"javascriptreact",
"typescript",
@ -18,19 +70,25 @@
"html",
"markdown",
"json",
"yaml"
"jsonc",
"yaml",
"toml",
"xml",
"gql",
"graphql",
"astro",
"svelte",
"css",
"less",
"scss",
"pcss",
"postcss"
],
"[typescript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[javascript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[json]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"i18n-ally.localesPaths": ["src/locales/index.js", "src/locales/languages"],
"i18n-ally.sourceLanguage": "zh-CN",
"i18n-ally.localesPaths": [
"src/locales/index.js",
"src/locales/languages"
],
"i18n-ally.keystyle": "nested",
"i18n-ally.extract.ignored": [
"Switch",
@ -52,5 +110,4 @@
"cSpell.words": [
"bhsn"
],
"common-intellisense.ui": []
}

View File

@ -84,15 +84,18 @@ Windows 及 Linux 端内部集成了 Gnirehtet 用于提供 PC 到安卓设
- 镜像
- 录制
- OTG
- 摄像
- 录制相机
- 录制音频
- 相机
- 灵活启动
- OTG
### 设备交互栏
- 切换键
- 主屏幕
- 返回键
- 关闭屏幕(实验性)
- 通知栏
- 电源键
- 旋转屏幕
@ -312,6 +315,10 @@ Windows 及 Linux 端内部集成了 Gnirehtet 用于提供 PC 到安卓设
你需要自定义 `scrcpy` 以及 `adb` 的文件路径(确保具有可执行权限),如果用到反向供网则同样需要以同样方法配置 `gnirehtet`
### Could not execute "adb start-server"
这可能是因为安装路径中包含中文或特殊字符导致的,请尝试更改安装路径。
## 获得帮助
> 因为是开源项目 全靠爱发电 所以支持有限 更新节奏不固定

View File

@ -82,15 +82,18 @@ Gnirehtet встроен в приложения для Windows и Linux, что
- Зеркалирование
- Запись
- OTG
- Запись с камеры
- Запись аудио
- Камера
- Пользовательский
- Гибкий запуск
- OTG
### Панель взаимодействия с устройством
- Переключатель
- Домой
- Назад
- Выключение экрана (экспериментально)
- Уведомление
- Питание
- Поворот
@ -311,6 +314,10 @@ Gnirehtet встроен в приложения для Windows и Linux, что
Вам нужно настроить пользовательские пути к файлам для `scrcpy` и `adb` (убедившись, что у них есть разрешения на выполнение). Если вы используете обратный тетеринг, аналогично настройте `gnirehtet`.
### Could not execute "adb start-server"
Это может быть вызвано наличием китайских или специальных символов в пути установки. Попробуйте изменить путь установки.
## Получение помощи
> Поскольку это проект с открытым исходным кодом, полностью поддерживаемый пожертвованиями, поддержка ограничена, и обновления могут не выходить по фиксированному расписанию.

View File

@ -82,15 +82,18 @@ Refer to [scrcpy/doc/shortcuts](https://github.com/Genymobile/scrcpy/blob/master
- Mirror
- Recording
- OTG
- Recording Camera
- Recording Audio
- Camera
- Custom
- OTG
### Device Interaction Bar
- Switch
- Home
- Back
- Turn off screen (experimental)
- Notification
- Power
- Rotation
@ -311,6 +314,10 @@ Please try `disabling audio forwarding` feature through the `preferences setting
You need to customize the file paths for `scrcpy` and `adb` (ensuring they have executable permissions). If using reverse tethering, configure `gnirehtet` similarly.
### Could not execute "adb start-server"
This might be due to Chinese or special characters in the installation path. Please try changing the installation path.
## Getting Help
> As this is an open source project run entirely by donations, support is limited and updates may not be on a fixed schedule.

View File

@ -63,7 +63,6 @@ import { i18n } from '$/locales/index.js'
import localeModel from '$/plugins/element-plus/locale.js'
import { useDeviceStore, useThemeStore } from '$/store/index.js'
import { ElMessage } from 'element-plus'
const themeStore = useThemeStore()
const deviceStore = useDeviceStore()

View File

@ -10,7 +10,6 @@ export function initControlWindow(mainWindow) {
const controlWindow = new BrowserWindow({
icon: getLogoPath(),
parent: mainWindow,
width: 700,
minWidth: 700,
height: 28,

View File

@ -27,7 +27,7 @@
"vue": "3.4.21"
},
"devDependencies": {
"@antfu/eslint-config": "3.3.2",
"@antfu/eslint-config": "3.8.0",
"@devicefarmer/adbkit": "3.2.6",
"@electron-toolkit/preload": "3.0.1",
"@electron-toolkit/utils": "3.0.0",
@ -39,15 +39,15 @@
"@vitejs/plugin-vue": "5.0.4",
"@vueuse/core": "10.9.0",
"dayjs": "1.11.11",
"electron": "29.1.1",
"electron-builder": "24.13.3",
"electron": "33.0.2",
"electron-builder": "25.1.8",
"electron-context-menu": "4.0.4",
"electron-find-in-page": "1.0.8",
"electron-log": "5.2.0",
"electron-store": "9.0.0",
"electron-updater": "6.1.8",
"electron-updater": "6.3.9",
"element-plus": "2.8.2",
"eslint": "9.10.0",
"eslint": "9.13.0",
"fix-path": "4.0.0",
"fs-extra": "11.2.0",
"husky": "9.0.11",
@ -59,11 +59,11 @@
"rimraf": "^6.0.1",
"simple-git": "^3.27.0",
"unocss": "0.62.3",
"unplugin-auto-import": "0.18.2",
"unplugin-auto-import": "0.18.3",
"unplugin-vue-components": "0.27.4",
"vite": "5.1.5",
"vite-plugin-electron": "0.28.7",
"vite-plugin-electron-renderer": "0.14.5",
"vite-plugin-electron": "0.28.8",
"vite-plugin-electron-renderer": "0.14.6",
"vite-svg-loader": "5.1.0",
"vue-command": "35.2.1",
"vue-i18n": "9.13.1",

View File

@ -6,6 +6,7 @@
import { sleep } from '$/utils'
export default {
inheritAttrs: false,
props: {
row: {
type: Object,

View File

@ -14,6 +14,7 @@ export default {
components: {
DeployDialog,
},
inheritAttrs: false,
props: {
row: {
type: Object,

View File

@ -6,6 +6,7 @@
import { sleep } from '$/utils'
export default {
inheritAttrs: false,
props: {
row: {
type: Object,

View File

@ -6,8 +6,31 @@
import { sleep } from '$/utils'
import { openFloatControl } from '$/utils/device/index.js'
const recordModel = {
default: {
excludes: '',
command: '',
extname: config => config['--record-format'] || 'mp4',
},
audio: {
excludes: ['--video-source', '--no-audio', '--mouse'],
commands: ['--no-video', '--mouse=disabled'],
extname: config => config['--audio-record-format'] || 'opus',
},
camera: {
excludes: ['--video-source'],
commands: ['--video-source=camera'],
extname: config => config['--record-format'] || 'mp4',
},
}
export default {
inheritAttrs: false,
props: {
recordType: {
type: String,
default: 'default',
},
row: {
type: Object,
default: () => ({}),
@ -22,6 +45,11 @@ export default {
loading: false,
}
},
computed: {
activeModel() {
return recordModel[this.recordType]
},
},
methods: {
async handleClick() {
const row = this.row
@ -32,25 +60,36 @@ export default {
const savePath = this.getRecordPath(row)
const args = this.$store.preference.scrcpyParameter(row.id, {
isRecord: true,
excludes: ['--otg', '--mouse=aoa', '--keyboard=aoa', '--show-touches'],
let args = this.$store.preference.scrcpyParameter(row.id, {
isRecord: ['default', 'audio'].includes(this.recordType),
isCamera: ['camera'].includes(this.recordType),
excludes: [
'--otg',
'--mouse=aoa',
'--keyboard=aoa',
'--show-touches',
...this.activeModel.excludes,
],
})
args += ` ${this.activeModel.commands.join(' ')}`
console.log('args', args)
try {
const recording = this.$scrcpy.record(row.id, {
title: this.$store.device.getLabel(row, 'recording'),
savePath,
args,
stdout: this.onStdout,
stderr: this.onStderr,
})
await sleep(1 * 1000)
this.loading = false
openFloatControl(toRaw(this.row))
if (['default'].includes(this.$props.type)) {
openFloatControl(toRaw(this.row))
}
await recording
@ -65,20 +104,21 @@ export default {
}
}
},
onStdout() {},
onStderr() {},
getRecordPath(row) {
const config = this.$store.preference.getData(row.id)
const basePath = config.savePath
const extension = config['--record-format'] || 'mp4'
const deviceConfig = this.$store.preference.getData(this.row.id)
const savePath = deviceConfig.savePath
const extension = this.activeModel.extname(deviceConfig)
const fileName = this.$store.device.getLabel(
row,
({ time }) => `record-${time}.${extension}`,
)
const joinValue = this.$path.join(basePath, fileName)
const value = this.$path.normalize(joinValue)
const filePath = this.$path.join(savePath, fileName)
const value = this.$path.normalize(filePath)
return value
},

View File

@ -1,96 +0,0 @@
<template>
<slot :loading="loading" :trigger="handleClick" />
</template>
<script>
import { sleep } from '$/utils'
import { openFloatControl } from '$/utils/device/index.js'
export default {
props: {
row: {
type: Object,
default: () => ({}),
},
toggleRowExpansion: {
type: Function,
default: () => () => false,
},
},
data() {
return {
loading: false,
}
},
methods: {
async handleClick() {
const row = this.row
this.loading = true
this.toggleRowExpansion(row, true)
const savePath = this.getRecordPath(row)
let args = this.$store.preference.scrcpyParameter(row.id, {
isRecord: true,
excludes: ['--otg', '--mouse=aoa', '--keyboard=aoa', '--video-source', '--show-touches'],
})
args += ' --video-source=camera'
try {
const recording = this.$scrcpy.record(row.id, {
title: this.$store.device.getLabel(row, 'recording'),
savePath,
args,
stdout: this.onStdout,
stderr: this.onStderr,
})
await sleep(1 * 1000)
this.loading = false
openFloatControl(toRaw(this.row))
await recording
await this.handleSuccess(savePath)
}
catch (error) {
console.error('record.args', args)
console.error('record.error', error)
if (error.message) {
this.$message.warning(error.message)
}
}
},
onStdout() {},
onStderr() {},
getRecordPath(row) {
const config = this.$store.preference.getData(row.id)
const basePath = config.savePath
const extension = config['--record-format'] || 'mp4'
const fileName = this.$store.device.getLabel(
row,
({ time }) => `record-${time}.${extension}`,
)
const joinValue = this.$path.join(basePath, fileName)
const value = this.$path.normalize(joinValue)
return value
},
async handleSuccess(savePath) {
return this.$message.success(
`${this.$t('device.record.success.title')}: ${savePath}`,
)
},
},
}
</script>
<style></style>

View File

@ -17,6 +17,7 @@
:key="index"
v-bind="{
...$props,
...(item.props || {}),
}"
v-slot="{ loading, trigger }"
>
@ -40,7 +41,6 @@
<script>
import Record from './components/Record/index.vue'
import Camera from './components/Camera/index.vue'
import RecordCamera from './components/RecordCamera/index.vue'
import Otg from './components/Otg/index.vue'
import Custom from './components/Custom/index.vue'
@ -48,7 +48,6 @@ export default {
components: {
Record,
Camera,
RecordCamera,
Otg,
Custom,
},
@ -66,12 +65,22 @@ export default {
component: 'Record',
},
{
label: 'device.actions.more.camera.name',
component: 'Camera',
label: 'device.actions.more.recordCamera.name',
component: 'Record',
props: {
recordType: 'camera',
},
},
{
label: 'device.actions.more.recordCamera.name',
component: 'RecordCamera',
label: 'device.actions.more.recordAudio.name',
component: 'Record',
props: {
recordType: 'audio',
},
},
{
label: 'device.actions.more.camera.name',
component: 'Camera',
},
{
label: 'device.actions.more.otg.name',

View File

@ -253,21 +253,21 @@ export default {
await this.$confirm(
`<div class="pt-4 pl-4">
<div class="text-sm text-red-500 pb-4">${this.$t(
'device.wireless.connect.error.detail',
)}${message}</div>
'device.wireless.connect.error.detail',
)}${message}</div>
<div>${this.$t('device.wireless.connect.error.reasons[0]')}</div>
<div>1. ${this.$t(
'device.wireless.connect.error.reasons[1]',
)} </div>
'device.wireless.connect.error.reasons[1]',
)} </div>
<div>2. ${this.$t(
'device.wireless.connect.error.reasons[2]',
)} </div>
'device.wireless.connect.error.reasons[2]',
)} </div>
<div>3. ${this.$t(
'device.wireless.connect.error.reasons[3]',
)} </div>
'device.wireless.connect.error.reasons[3]',
)} </div>
<div>4. ${this.$t(
'device.wireless.connect.error.reasons[4]',
)} </div>
'device.wireless.connect.error.reasons[4]',
)} </div>
</div>`,
this.$t('device.wireless.connect.error.title'),
{

View File

@ -1,6 +1,6 @@
<template>
<el-select
v-bind="{ ...(data.props || {}) }"
v-bind="{ clearable: true, ...(data.props || {}) }"
v-model="selectValue"
class="!w-full"
>

View File

@ -1,6 +1,7 @@
<template>
<el-select
v-bind="{
clearable: true,
...(data.props || {}),
}"
v-model="selectValue"

View File

@ -1,6 +1,6 @@
<template>
<el-select
v-bind="{ ...(data.props || {}) }"
v-bind="{ clearable: true, ...(data.props || {}) }"
v-model="selectValue"
class="!w-full"
>

View File

@ -1,5 +1,5 @@
<template>
<el-form ref="elForm" :model="preferenceData" label-width="225px" class="">
<el-form ref="elForm" :model="preferenceData" label-width="250px" class="">
<el-collapse
v-model="collapseValue"
v-bind="{
@ -31,61 +31,52 @@
</div>
</div>
</template>
<div class="pt-4">
<el-form
ref="elForm"
:model="preferenceData"
label-width="250px"
class="pr-8 pt-4"
>
<el-row :gutter="20">
<el-col
v-for="(item_1, name_1) of subModel(item)"
:key="name_1"
:span="item_1.span || 12"
:offset="item_1.offset || 0"
>
<el-form-item :label="$t(item_1.label)" :prop="item_1.field">
<template #label>
<div class="flex items-center">
<el-tooltip
v-if="item_1.tips"
popper-class="max-w-96"
effect="dark"
:content="$t(item_1.tips)"
placement="bottom"
<div class="pr-8 pt-4">
<el-row :gutter="20">
<el-col
v-for="(item_1, name_1) of subModel(item)"
:key="name_1"
:span="item_1.span || 12"
:offset="item_1.offset || 0"
>
<el-form-item :label="$t(item_1.label)" :prop="item_1.field">
<template #label>
<div class="flex items-center">
<el-tooltip
v-if="item_1.tips"
popper-class="max-w-96"
effect="dark"
:content="$t(item_1.tips)"
placement="bottom"
>
<el-link
class="mr-1 !text-base"
icon="InfoFilled"
type="warning"
:underline="false"
>
<el-link
class="mr-1 !text-base"
icon="InfoFilled"
type="warning"
:underline="false"
>
</el-link>
</el-tooltip>
<div class="truncate max-w-56" :title="$t(item_1.label)">
{{
$t(item_1.label)
}}
</div>
</el-link>
</el-tooltip>
<div class="truncate max-w-56" :title="$t(item_1.label)">
{{ $t(item_1.label) }}
</div>
</template>
</div>
</template>
<component
:is="inputModel[item_1.type]"
v-model="preferenceData[item_1.field]"
v-bind="{
preferenceData,
deviceScope,
title: $t(item_1.placeholder),
placeholder: $t(item_1.placeholder),
data: item_1,
}"
></component>
</el-form-item>
</el-col>
</el-row>
</el-form>
<component
:is="inputModel[item_1.type]"
v-model="preferenceData[item_1.field]"
v-bind="{
preferenceData,
deviceScope,
title: $t(item_1.placeholder),
placeholder: $t(item_1.placeholder),
data: item_1,
}"
></component>
</el-form-item>
</el-col>
</el-row>
</div>
</el-collapse-item>
</el-collapse>
@ -93,9 +84,12 @@
</template>
<script setup>
import { i18n } from '$/locales/index.js'
import { usePreferenceStore } from '$/store/index.js'
import { omit } from 'lodash-es'
import { computed } from 'vue'
import { inputModel } from './components/index.js'
@ -114,6 +108,8 @@ const props = defineProps({
},
})
const locale = computed(() => i18n.global.locale.value)
const preferenceData = defineModel('modelValue', {
type: Object,
default: () => ({}),

View File

@ -129,6 +129,7 @@
"device.actions.more.record.name": "Start Recording",
"device.actions.more.camera.name": "Startup Camera",
"device.actions.more.recordCamera.name": "Record Camera",
"device.actions.more.recordAudio.name": "Record Audio",
"device.actions.more.otg.name": "Startup OTG",
"device.actions.more.custom.name": "Custom Startup",
@ -317,9 +318,11 @@
"preferences.record.name": "Recording",
"preferences.record.format.name": "Format",
"preferences.record.format.placeholder": "mp4",
"preferences.record.format.audio.name": "Audio Format",
"preferences.record.format.audio.placeholder": "opus",
"preferences.record.time-limit.name": "Recording Time Limit",
"preferences.record.time-limit.placeholder": "No time limit",
"preferences.record.format.placeholder": "mp4",
"preferences.record.lock-video-orientation.name": "Video Direction",
"preferences.record.lock-video-orientation.placeholder": "Device Orientation",
"preferences.record.no-video-playback.name": "Disable Video Playback",

View File

@ -129,6 +129,7 @@
"device.actions.more.record.name": "Начать запись",
"device.actions.more.camera.name": "Запустить камеры",
"device.actions.more.recordCamera.name": "Запись камеры",
"device.actions.more.recordAudio.name": "Записать аудио",
"device.actions.more.otg.name": "Запустить OTG",
"device.actions.more.custom.name": "Пользовательский запуск",
@ -317,9 +318,11 @@
"preferences.record.name": "Запись",
"preferences.record.format.name": "Формат",
"preferences.record.format.placeholder": "mp4",
"preferences.record.format.audio.name": "Аудио формат",
"preferences.record.format.audio.placeholder": "opus",
"preferences.record.time-limit.name": "Ограничение времени записи",
"preferences.record.time-limit.placeholder": "Без ограничения времени",
"preferences.record.format.placeholder": "mp4",
"preferences.record.lock-video-orientation.name": "Ориентация видео",
"preferences.record.lock-video-orientation.placeholder": "Ориентация устройства",
"preferences.record.no-video-playback.name": "Отключить воспроизведение видео",

View File

@ -129,6 +129,7 @@
"device.actions.more.record.name": "开始录制",
"device.actions.more.camera.name": "启动相机",
"device.actions.more.recordCamera.name": "录制相机",
"device.actions.more.recordAudio.name": "录制音频",
"device.actions.more.otg.name": "启动OTG",
"device.actions.more.custom.name": "灵活启动",
@ -316,8 +317,10 @@
"preferences.window.position.y.placeholder": "相对于桌面中心",
"preferences.record.name": "音视频录制",
"preferences.record.format.name": "录制视频格式",
"preferences.record.format.name": "视频格式",
"preferences.record.format.placeholder": "mp4",
"preferences.record.format.audio.name": "音频格式",
"preferences.record.format.audio.placeholder": "opus",
"preferences.record.time-limit.name": "录制时长",
"preferences.record.time-limit.placeholder": "不限时长",
"preferences.record.lock-video-orientation.name": "录制视频方向",

View File

@ -129,6 +129,7 @@
"device.actions.more.record.name": "開始錄製",
"device.actions.more.camera.name": "啟動鏡頭",
"device.actions.more.recordCamera.name": "錄製鏡頭",
"device.actions.more.recordAudio.name": "錄製音訊",
"device.actions.more.otg.name": "啟動 OTG",
"device.actions.more.custom.name": "靈活啟動",
@ -316,8 +317,10 @@
"preferences.window.position.y.placeholder": "相對於桌面中心",
"preferences.record.name": "影片錄製",
"preferences.record.format.name": "錄製影片格式",
"preferences.record.format.name": "影片格式",
"preferences.record.format.placeholder": "mp4",
"preferences.record.format.audio.name": "音訊格式",
"preferences.record.format.audio.placeholder": "opus",
"preferences.record.time-limit.name": "錄製時長",
"preferences.record.time-limit.placeholder": "不限時長",
"preferences.record.lock-video-orientation.name": "錄製影片方向",

View File

@ -46,6 +46,7 @@ export const usePreferenceStore = defineStore({
'--video-code',
'--audio-code',
'--keyboard-inject',
'--audio-record-format',
...getOtherFields('scrcpy'),
],
recordKeys,

View File

@ -19,6 +19,39 @@ export default {
},
],
},
audioRecordFormat: {
label: 'preferences.record.format.audio.name',
field: '--audio-record-format',
type: 'Select',
value: void 0,
placeholder: 'preferences.record.format.audio.placeholder',
options: [
{
label: 'opus',
value: 'opus',
},
{
label: 'wav',
value: 'wav',
},
{
label: 'mka',
value: 'mka',
},
{
label: 'flac',
value: 'flac',
},
{
label: 'aac',
value: 'aac',
},
{
label: 'm4a',
value: 'm4a',
},
],
},
lockVideoOrientation: {
label: 'preferences.record.lock-video-orientation.name',
field: '--lock-video-orientation',