perf: ♻️ optimize Codec

This commit is contained in:
viarotel 2023-11-02 15:08:19 +08:00
parent 5331eb1396
commit 6154ffcfae
13 changed files with 371 additions and 71 deletions

View File

@ -11,5 +11,6 @@ module.exports = {
'import/default': 'off',
'vue/no-mutating-props': 'off',
'vue/no-use-v-if-with-v-for': 'off',
},
}

View File

@ -1,7 +1,10 @@
import { spawn } from 'node:child_process'
import util from 'node:util'
import { exec as _exec, spawn } from 'node:child_process'
import appStore from '@electron/helpers/store.js'
import { adbPath, scrcpyPath } from '@electron/configs/index.js'
const exec = util.promisify(_exec)
const shell = async (command, { stdout, stderr } = {}) => {
const spawnPath = appStore.get('common.scrcpyPath') || scrcpyPath
const ADB = appStore.get('common.adbPath') || adbPath
@ -9,6 +12,7 @@ const shell = async (command, { stdout, stderr } = {}) => {
console.log('scrcpy.shell.spawnPath', spawnPath)
console.log('scrcpy.shell.ADB', ADB)
console.log('scrcpy.shell.args', args)
const scrcpyProcess = spawn(`"${spawnPath}"`, args, {
env: { ...process.env, ADB },
@ -52,6 +56,54 @@ const shell = async (command, { stdout, stderr } = {}) => {
})
}
const execShell = async (command) => {
const spawnPath = appStore.get('common.scrcpyPath') || scrcpyPath
const ADB = appStore.get('common.adbPath') || adbPath
console.log('scrcpy.execShell.spawnPath', spawnPath)
console.log('scrcpy.execShell.ADB', ADB)
console.log('scrcpy.shell.command', command)
const res = exec(`"${spawnPath}" ${command}`, {
env: { ...process.env, ADB },
shell: true,
encoding: 'utf8',
})
return res
}
const getEncoders = async (serial) => {
const res = await execShell(`--serial="${serial}" --list-encoders`)
// console.log('getEncoders.res', res)
const stdout = res.stdout
// 提取视频编码器列表
const videoEncoderRegex
= /--video-codec=([\w-]+)\s+--video-encoder='([^']+)'/g
const videoEncoders = [...stdout.matchAll(videoEncoderRegex)].map(
([, codec, encoder]) => ({ decoder: codec, encoder }),
)
// 提取音频编码器列表
const audioEncoderRegex
= /--audio-codec=([\w-]+)\s+--audio-encoder='([^']+)'/g
const audioEncoders = [...stdout.matchAll(audioEncoderRegex)].map(
([, codec, encoder]) => ({ decoder: codec, encoder }),
)
const value = {
audio: audioEncoders,
video: videoEncoders,
}
console.log('getEncoders.value', value)
return value
}
export default () => ({
shell,
execShell,
getEncoders,
})

View File

@ -0,0 +1,89 @@
<template>
<el-select
v-bind="data.props || {}"
:model-value="modelValue"
class="!w-full"
:title="$t(data.placeholder)"
:placeholder="$t(data.placeholder)"
@change="onChange"
>
<el-option
v-for="(item, index) in options"
:key="index"
:label="$t(item.label)"
:value="item.value"
>
</el-option>
</el-select>
</template>
<script>
export default {
props: {
modelValue: {
type: String,
value: '',
},
data: {
type: Object,
default: () => ({}),
},
deviceScope: {
type: String,
value: '',
},
preferenceData: {
type: Object,
default: () => ({}),
},
},
emits: ['update:model-value'],
data() {
return {
deviceOptions: [],
}
},
computed: {
options() {
return this.deviceOptions.length ? this.deviceOptions : this.data.options
},
},
watch: {
deviceScope: {
handler(value) {
if (value === 'global') {
this.deviceOptions = []
return
}
this.getDeviceOptions()
},
},
},
methods: {
async getDeviceOptions() {
const res = await this.$scrcpy.getEncoders(this.deviceScope)
this.deviceOptions = res?.audio?.map((item) => {
const value = `${item.decoder} & ${item.encoder}`
return {
label: value,
value,
}
})
console.log('deviceOptions', this.deviceOptions)
},
onChange(value) {
// console.log('value', value)
this.$emit('update:model-value', value)
const [decoder, encoder] = value.split(' & ')
this.preferenceData['--audio-codec'] = decoder
this.preferenceData['--audio-encoder'] = encoder
},
},
}
</script>
<style></style>

View File

@ -0,0 +1,89 @@
<template>
<el-select
v-bind="data.props || {}"
:model-value="modelValue"
class="!w-full"
:title="$t(data.placeholder)"
:placeholder="$t(data.placeholder)"
@change="onChange"
>
<el-option
v-for="(item, index) in options"
:key="index"
:label="$t(item.label)"
:value="item.value"
>
</el-option>
</el-select>
</template>
<script>
export default {
props: {
modelValue: {
type: String,
value: '',
},
data: {
type: Object,
default: () => ({}),
},
deviceScope: {
type: String,
value: '',
},
preferenceData: {
type: Object,
default: () => ({}),
},
},
emits: ['update:model-value'],
data() {
return {
deviceOptions: [],
}
},
computed: {
options() {
return this.deviceOptions.length ? this.deviceOptions : this.data.options
},
},
watch: {
deviceScope: {
handler(value) {
if (value === 'global') {
this.deviceOptions = []
return
}
this.getDeviceOptions()
},
},
},
methods: {
async getDeviceOptions() {
const res = await this.$scrcpy.getEncoders(this.deviceScope)
this.deviceOptions = res?.video?.map((item) => {
const value = `${item.decoder} & ${item.encoder}`
return {
label: value,
value,
}
})
console.log('deviceOptions', this.deviceOptions)
},
onChange(value) {
// console.log('value', value)
this.$emit('update:model-value', value)
const [decoder, encoder] = value.split(' & ')
this.preferenceData['--video-codec'] = decoder
this.preferenceData['--video-encoder'] = encoder
},
},
}
</script>
<style></style>

View File

@ -1,6 +1,6 @@
import { watchEffect } from 'vue'
export function useOTG(data) {
export function useOtg(data) {
watchEffect(() => {
if (data.value['--hid-keyboard'] || data.value['--hid-mouse']) {
data.value['--otg'] = true

View File

@ -83,7 +83,7 @@
>
<el-row :gutter="20">
<el-col
v-for="(item_1, index_1) of item?.children || {}"
v-for="(item_1, index_1) of subModel(item)"
:key="index_1"
:span="12"
:offset="0"
@ -170,6 +170,8 @@
v-else
v-model="preferenceData[item_1.field]"
:data="item_1"
:device-scope="deviceScope"
:preference-data="preferenceData"
></component>
</el-form-item>
</el-col>
@ -184,9 +186,11 @@
<script>
import { debounce } from 'lodash-es'
import { ref } from 'vue'
import { useOTG } from './__composables__/OTG/index.js'
import { useOtg } from './__composables__/otg/index.js'
import LanguageSelect from './LanguageSelect/index.vue'
import PathInput from './PathInput/index.vue'
import VideoCodecSelect from './VideoCodecSelect/index.vue'
import AudioCodecSelect from './AudioCodecSelect/index.vue'
import LoadingIcon from '@/components/Device/ControlBar/LoadingIcon/index.vue'
import { usePreferenceStore } from '@/store/index.js'
@ -194,6 +198,8 @@ export default {
components: {
LanguageSelect,
PathInput,
VideoCodecSelect,
AudioCodecSelect,
},
setup() {
const preferenceStore = usePreferenceStore()
@ -201,7 +207,7 @@ export default {
const preferenceData = ref(preferenceStore.data)
const deviceScope = ref(preferenceStore.deviceScope)
useOTG(preferenceData)
useOtg(preferenceData)
return {
preferenceData,
@ -218,7 +224,6 @@ export default {
label: `${item.id}${item.$name}${
item.$remark ? `${item.$remark}` : ''
}`,
value: item.id,
}))
@ -269,6 +274,16 @@ export default {
this.getDisplay()
},
methods: {
subModel(item) {
const children = item?.children || {}
const value = {}
Object.entries(children).forEach(([key, data]) => {
if (!data.hidden) {
value[key] = data
}
})
return value
},
handleResetAll() {
this.$store.preference.reset(this.deviceScope)
this.preferenceData = this.$store.preference.data

View File

@ -138,10 +138,8 @@
"preferences.video.bit.placeholder": "Default 4M, equal to 4000000",
"preferences.video.refresh-rate.name": "Frame Rate",
"preferences.video.refresh-rate.placeholder": "Default 60",
"preferences.video.decoder.name": "Video Decoder",
"preferences.video.decoder.placeholder": "Default h264",
"preferences.video.encoder.name": "Video Encoder",
"preferences.video.encoder.placeholder": "Default device encoder",
"preferences.video.codec.name": "Video Codec",
"preferences.video.codec.placeholder": "Default H.264",
"preferences.video.screen-rotation.name": "Rotation",
"preferences.video.screen-rotation.placeholder": "Default device rotation",
"preferences.video.screen-cropping.name": "Crop",

View File

@ -4,6 +4,7 @@
"common.cancel": "取消",
"common.confirm": "确认",
"common.restart": "重启",
"common.default": "默认",
"common.tips": "提示",
"common.open": "打开",
"common.input.placeholder": "请填写",
@ -138,10 +139,8 @@
"preferences.video.bit.placeholder": "默认值为 4M,等同于 4000000",
"preferences.video.refresh-rate.name": "刷新率",
"preferences.video.refresh-rate.placeholder": "默认值为 60",
"preferences.video.decoder.name": "视频解码器",
"preferences.video.decoder.placeholder": "默认值为 h264",
"preferences.video.encoder.name": "视频编码器",
"preferences.video.encoder.placeholder": "默认值为设备默认编码器",
"preferences.video.codec.name": "视频编码",
"preferences.video.codec.placeholder": "默认为 H.264",
"preferences.video.screen-rotation.name": "屏幕旋转",
"preferences.video.screen-rotation.placeholder": "默认值为设备屏幕旋转角度",
"preferences.video.screen-cropping.name": "屏幕裁剪",
@ -181,12 +180,21 @@
"preferences.record.format.name": "录制视频格式",
"preferences.record.format.placeholder": "默认为 *.mp4 格式",
"preferences.audio.name": "音频控制",
"preferences.audio.disable.name": "禁用音频",
"preferences.audio.disable.placeholder": "开启后将禁用音频功能",
"preferences.audio.audio-source.name": "音频源",
"preferences.audio.audio-source.placeholder": "默认为设备音频输出",
"preferences.audio.audio-source.tips": "技巧:如果将来源设为麦克风将可以在录制时将声音录制下来",
"preferences.audio.audio-source.mic": "麦克风",
"preferences.audio.audio-codec.name": "音频编解码器",
"preferences.audio.audio-codec.placeholder": "默认为 Opus",
"preferences.audio.audio-bit-rate.name": "音频比特率",
"preferences.audio.audio-bit-rate.placeholder": "默认为 128000bps",
"preferences.audio.audio-bit-rate.tips": "注意:此选项不适用于 RAW 音频编解码器",
"preferences.audio.audio-buffer.name": "音频缓冲",
"preferences.audio.audio-buffer.placeholder": "默认值为 0ms",
"preferences.audio.audio-output-buffer.name": "音频输出缓冲",
"preferences.audio.audio-output-buffer.placeholder": "默认值为 5ms",
"preferences.audio.disable.name": "禁用音频",
"preferences.audio.disable.placeholder": "开启后将禁用音频功能",
"preferences.otg.name": "OTG 控制",
"preferences.otg.enable.name": "启用 OTG",
"preferences.otg.enable.placeholder": "开启或关闭 OTG 功能",

View File

@ -10,7 +10,7 @@ import icons from './icons/index.js'
import { i18n, t } from './locales/index.js'
import { replaceIP } from '@/utils/index.js'
import { replaceIP, restoreIP } from '@/utils/index.js'
import 'virtual:uno.css'
import './styles/index.js'
@ -36,6 +36,7 @@ app.config.globalProperties.$scrcpy = window.scrcpy
app.config.globalProperties.$gnirehtet = window.gnirehtet
app.config.globalProperties.$replaceIP = replaceIP
app.config.globalProperties.$restoreIP = restoreIP
app.mount('#app').$nextTick(() => {
// Remove Preload scripts loading

View File

@ -11,19 +11,27 @@ import {
setStoreData,
} from './helpers/index.js'
import { replaceIP } from '@/utils/index.js'
import { replaceIP, restoreIP } from '@/utils/index.js'
const { adbPath, scrcpyPath } = window.electron?.configs || {}
export const usePreferenceStore = defineStore({
id: 'app-preference',
state() {
const deviceScope = restoreIP(
window.appStore.get('scrcpy.deviceScope') || 'global',
)
return {
model: cloneDeep(model),
data: { ...getDefaultData() },
deviceScope: window.appStore.get('scrcpy.deviceScope') || 'global',
scrcpyExcludeKeys: ['--record-format', ...getOtherFields('scrcpy')],
deviceScope,
scrcpyExcludeKeys: [
'--record-format',
'--video-code',
'--audio-code',
...getOtherFields('scrcpy'),
],
}
},
getters: {},
@ -114,7 +122,7 @@ export const usePreferenceStore = defineStore({
return ''
}
const value = Object.entries(data).reduce((arr, [key, value]) => {
const valueList = Object.entries(data).reduce((arr, [key, value]) => {
if (!value) {
return arr
}
@ -133,11 +141,11 @@ export const usePreferenceStore = defineStore({
return arr
}, [])
const joinValue = value.join(' ')
const value = valueList.join(' ')
console.log('getScrcpyData.joinValue', joinValue)
console.log('getScrcpyData.value', value)
return joinValue
return value
},
getModel(path) {
const value = get(this.model, path)

View File

@ -3,6 +3,58 @@ export default {
field: 'scrcpy',
children: {
audioSource: {
label: 'preferences.audio.audio-source.name',
field: '--audio-source',
type: 'Select',
value: '',
placeholder: 'preferences.audio.audio-source.placeholder',
tips: 'preferences.audio.audio-source.tips',
options: [
{ label: 'common.default', value: '' },
{ label: '麦克风', value: 'mic' },
],
},
audioCode: {
label: 'preferences.audio.audio-codec.name',
field: '--audio-code',
type: 'AudioCodecSelect',
value: '',
placeholder: 'preferences.audio.audio-codec.placeholder',
options: [
{
label: 'opus & c2.android.opus.encoder',
value: 'opus & c2.android.opus.encoder',
},
{
label: 'aac & c2.android.aac.encoder',
value: 'aac & c2.android.aac.encoder',
},
{
label: 'aac & OMX.google.aac.encoder',
value: 'aac & OMX.google.aac.encoder',
},
{ label: 'raw', value: 'raw' },
],
},
audioCodec: {
hidden: true,
field: '--audio-codec',
value: '',
},
audioEncoder: {
hidden: true,
field: '--audio-encoder',
value: '',
},
audioBitRate: {
label: 'preferences.audio.audio-bit-rate.name',
field: '--audio-bit-rate',
type: 'Input.number',
value: '',
placeholder: 'preferences.audio.audio-bit-rate.placeholder',
append: 'bps',
},
audioBuffer: {
label: 'preferences.audio.audio-buffer.name',
field: '--audio-buffer',

View File

@ -25,61 +25,44 @@ export default {
placeholder: 'preferences.video.refresh-rate.placeholder',
append: 'fps',
},
videoCodec: {
label: 'preferences.video.decoder.name',
field: '--video-codec',
type: 'Select',
videoCode: {
label: 'preferences.video.codec.name',
field: '--video-code',
type: 'VideoCodecSelect',
value: '',
placeholder: 'preferences.video.decoder.placeholder',
placeholder: 'preferences.video.codec.placeholder',
options: [
{
label: 'h264',
value: 'h264',
label: 'h265 & OMX.qcom.video.encoder.avc',
value: 'h265 & OMX.qcom.video.encoder.avc',
},
{
label: 'h265',
value: 'h265',
label: 'h265 & c2.android.avc.encoder',
value: 'h265 & c2.android.avc.encoder',
},
{
label: 'av1',
value: 'av1',
label: 'h264 & OMX.google.h264.encoder',
value: 'h264 & OMX.google.h264.encoder',
},
{
label: 'h264 & OMX.qcom.video.encoder.hevc',
value: 'h264 & OMX.qcom.video.encoder.hevc',
},
{
label: 'h264 & c2.android.hevc.encoder',
value: 'h264 & c2.android.hevc.encoder',
},
],
},
videoEncoder: {
label: 'preferences.video.encoder.name',
field: '--video-encoder',
type: 'Select',
videoCodec: {
hidden: true,
field: '--video-codec',
value: '',
},
videoEncoder: {
hidden: true,
field: '--video-encoder',
value: '',
placeholder: 'preferences.video.encoder.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'"
// "--video-codec=h264 --video-encoder='OMX.google.h264.encoder'"
// "--video-codec=h265 --video-encoder='OMX.qcom.video.encoder.hevc'"
// "--video-codec=h265 --video-encoder='c2.android.hevc.encoder'"
options: [
{
label: 'Android HEVC(H.265) ',
value: 'OMX.qcom.video.encoder.avc',
},
{
label: 'Qualcomm HEVC(H.265) ',
value: 'c2.android.avc.encoder',
},
{
label: 'Google H.264(AVC)',
value: 'OMX.google.h264.encoder',
},
{
label: 'Android AVC(H.264) ',
value: 'OMX.qcom.video.encoder.hevc',
},
{
label: 'Qualcomm AVC(H.264)',
value: 'c2.android.hevc.encoder',
},
],
},
rotation: {
label: 'preferences.video.screen-rotation.name',

View File

@ -17,8 +17,12 @@ export function isIPWithPort(ip) {
return regex.test(ip)
}
export function replaceIP(value, to = '_') {
return value.replaceAll('.', to).replaceAll(':', to)
export function replaceIP(value) {
return value.replaceAll('.', '_').replaceAll(':', '-')
}
export function restoreIP(value) {
return value.replaceAll('_', '.').replaceAll('-', ':')
}
/**