Skip to content

Commit

Permalink
Add windows support
Browse files Browse the repository at this point in the history
  • Loading branch information
krypciak committed Jun 12, 2024
1 parent d1ef472 commit 9408ea1
Show file tree
Hide file tree
Showing 6 changed files with 126 additions and 59 deletions.
10 changes: 5 additions & 5 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

54 changes: 42 additions & 12 deletions src/audio.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,19 @@
import ffmpeg from 'fluent-ffmpeg'
import ffmpeg, { FfmpegCommand } from 'fluent-ffmpeg'
import CCRecord from './plugin'
import { sccPath, wdarPath } from './fs-misc'

type Killable = {
kill(...args: any[]): void
}

const exec = (require('child_process') as typeof import('child_process')).exec

export class CCAudioRecorder {
private fragmentSize!: number
private ffmpegInstance!: ffmpeg.FfmpegCommand
private justKilledPolitely: boolean = false

private childProcess?: Killable

private finishPromise!: Promise<void>

audioPath!: string
Expand All @@ -22,20 +30,17 @@ export class CCAudioRecorder {
this.fragmentSize = fragmentSize

if (process.platform == 'linux') {
this.ffmpegInstance = await this.recordLinux(audioPath, this.fragmentSize)
this.childProcess = await this.recordLinux(audioPath, this.fragmentSize)
} else if (process.platform == 'win32') {
this.childProcess = await this.recordWindows(audioPath, this.fragmentSize)
}

if (this.ffmpegInstance) {
if (CCRecord.log) console.log(`${this.ccrecord.recordIndex}: started audio recording`)
}
if (CCRecord.log) console.log(`${this.ccrecord.recordIndex}: started audio recording`)
}

private async recordLinux(outFilePath: string, duration: number): Promise<ffmpeg.FfmpegCommand> {
// ffmpeg -f pulse -i 71 -acodec copy output.wav
//ffmpeg()
private async recordLinux(outFilePath: string, duration: number): Promise<FfmpegCommand> {
async function getActiveSource(): Promise<number> {
return new Promise<number>(resolve => {
const { exec } = require('child_process')
const command = `pactl list short sources | grep 'RUNNING' | awk '{print $1}' | head --lines 1`

exec(command, (_error: any, stdout: any, _stderr: any) => {
Expand Down Expand Up @@ -68,13 +73,38 @@ export class CCAudioRecorder {
.saveToFile(outFilePath)
}

private async recordWindows(outFilePath: string, duration: number): Promise<Killable> {
let resolve: () => void
this.finishPromise = new Promise<void>(r => {
resolve = r
})

const child = exec(
`.\\${wdarPath.replace(/\//g, '\\')} --output ${outFilePath} --time ${duration}`,
(_error: any, _stdout: string, _stderr: any) => {
// console.log('\nerror: ', _error, '\nstdout: ', _stdout, '\n_stderr: ', _stderr)
resolve()
}
)
return {
kill(msg: string, ..._args) {
if (msg == 'SIGTERM') {
exec(`.\\${sccPath.replace(/\//g, '\\')} ${child.pid}`)
} else if (msg == 'SIGKILL') {
exec(`taskkill /im windows-desktop-audio-recorder.exe /t /f`)
}
},
}
}

async stopRecording() {
this.justKilledPolitely = true
this.ffmpegInstance?.kill('SIGTERM')
this.childProcess?.kill('SIGTERM')

await this.finishPromise
}

async terminate() {
this.ffmpegInstance?.kill('SIGKILL')
this.childProcess?.kill('SIGKILL')
}
}
52 changes: 41 additions & 11 deletions src/fs-misc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ async function doesFileExist(path: string): Promise<boolean> {
})
}

/* FFmpeg */
let ffmpegPath!: string

async function isFFmpegInstalledNow(): Promise<boolean> {
Expand Down Expand Up @@ -54,7 +55,6 @@ async function unpackFileFromZip(data: ArrayBuffer, fileName: string, outFilePat
return fs.promises.writeFile(outFilePath, fileData)
}

/** does not require administrator privilages */
export async function installFFmpegWindows() {
const ffmpegZipPath = `${CCRecord.baseDataPath}/temp/ffmpeg.zip`
let data: ArrayBuffer
Expand All @@ -69,13 +69,43 @@ export async function installFFmpegWindows() {
return unpackFileFromZip(data, 'ffmpeg.exe', ffmpegPath)
}

// private testCommandReturnStats(command: string): Promise<boolean> {
// const { exec } = require('child_process')
//
// return new Promise(resolve => {
// exec(command, { encoding: 'utf8' }, (error: any, stdout: any) => {
// console.log(error, stdout)
// return !error
// })
// })
// }
/* Windows Desktop Audio Recorder (wdar) https://github.com/krypciak/windows-desktop-audio-recorder */
export let wdarPath: string

export async function isWdarInstalled(): Promise<boolean> {
wdarPath = `${CCRecord.baseDataPath}/windows-desktop-audio-recorder.exe`
return doesFileExist(wdarPath)
}

export async function installWdar() {
const wdarZipPath = `${CCRecord.baseDataPath}/temp/windows-desktop-audio-recorder.zip`
let data: ArrayBuffer
if (!(await doesFileExist(wdarZipPath))) {
const url =
'https://github.com/krypciak/windows-desktop-audio-recorder/releases/download/v1.0.0/windows-desktop-audio-recorder.zip'
const resp = await fetch(url)
data = await resp.arrayBuffer()
} else {
data = await fs.promises.readFile(wdarZipPath)
}

return unpackFileFromZip(data, 'windows-desktop-audio-recorder.exe', wdarPath)
}

/* send_ctrl_c (scc) (used to stop wdar) https://gist.github.com/rdp/f51fb274d69c5c31b6be */

export let sccPath: string

export async function isSccInstalled(): Promise<boolean> {
sccPath = `${CCRecord.baseDataPath}/send_ctrl_c.exe`
return doesFileExist(sccPath)
}

export async function installScc() {
let data: ArrayBuffer
const url = 'https://github.com/krypciak/windows-desktop-audio-recorder/releases/download/v1.0.0/send_ctrl_c.exe'
const resp = await fetch(url)
data = await resp.arrayBuffer()

return fs.promises.writeFile(sccPath, Buffer.from(data))
}
4 changes: 2 additions & 2 deletions src/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export const qualitySliderData: Record<number, number> = {
1: 500_000,
}

export let Opts: ReturnType<typeof sc.modMenu.registerAndGetModOptions<ReturnType<typeof registerOpts>>>
export let Opts: ReturnType<typeof modmanager.registerAndGetModOptions<ReturnType<typeof registerOpts>>>

export function registerOpts(ccrecord: CCRecord) {
const opts = {
Expand Down Expand Up @@ -69,7 +69,7 @@ export function registerOpts(ccrecord: CCRecord) {
},
} as const satisfies Options

Opts = sc.modMenu.registerAndGetModOptions(
Opts = modmanager.registerAndGetModOptions(
{
modId: 'cc-record',
title: 'cc-record',
Expand Down
62 changes: 36 additions & 26 deletions src/plugin.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,18 @@
import { PluginClass } from 'ultimate-crosscode-typedefs/modloader/mod'
import type { Mod1 } from 'ccmodmanager/src/types'
import type { Mod1 } from 'ccmodmanager/types/types'
import { Opts, registerOpts } from './options'
import { CCAudioRecorder } from './audio'
import { CCVideoRecorder } from './video'
import { postLinkToRequestBin, uploadFile } from './upload'
import { deleteFiles, installFFmpegWindows, isFFmpegInstalled } from './fs-misc'
import { makeSureVideoEncoderExistsAndIfNotCreateIt } from './videoencoder-polyfil'
import {
deleteFiles,
installFFmpegWindows,
installScc,
installWdar,
isFFmpegInstalled,
isSccInstalled,
isWdarInstalled as isWdarInstalled,
} from './fs-misc'

import ffmpeg from 'fluent-ffmpeg'

Expand Down Expand Up @@ -53,7 +60,6 @@ export default class CCRecord implements PluginClass {

registerOpts(this)

makeSureVideoEncoderExistsAndIfNotCreateIt()
this.videoRecorder = new CCVideoRecorder(this)
this.audioRecorder = new CCAudioRecorder(this)
}
Expand Down Expand Up @@ -82,31 +88,33 @@ export default class CCRecord implements PluginClass {
}, 300)
}

async initialRecordStart() {
let justTerminated: boolean = false
private async installNecceseryPrograms() {
const promises: Promise<void>[] = []
if (!(await isFFmpegInstalled())) {
this.terminateAll()
justTerminated = true

const baseText = 'FFmpeg is not installed.\ncc-record cannot work without FFmpeg installed.'
if (process.platform == 'win32') {
sc.Dialogs.showChoiceDialog(
`${baseText}\nDo you want to download and run the automatic installer now?`,
sc.DIALOG_INFO_ICON.QUESTION,
['No', 'Yes'],
button => {
if (button.data != 1) return
sc.Dialogs.showInfoDialog(`FFmpeg is being downloaded, please be patient.`)
installFFmpegWindows().then(() => {
sc.Dialogs.showInfoDialog(`FFmpeg installed.`)
})
}
)
promises.push(installFFmpegWindows())
} else {
sc.Dialogs.showErrorDialog(baseText)
sc.Dialogs.showErrorDialog('FFmpeg is not installed.\ncc-record cannot work without FFmpeg installed.')
this.terminated = true
return
}
}

if (process.platform == 'win32') {
if (!(await isWdarInstalled())) {
promises.push(installWdar())
}
if (!(await isSccInstalled())) {
promises.push(installScc())
}
}
if (this.terminated && !justTerminated) {
await Promise.all(promises)
}

async initialRecordStart() {
await this.installNecceseryPrograms()

if (this.terminated) {
this.playTerminatedSound()
return
}
Expand All @@ -125,7 +133,9 @@ export default class CCRecord implements PluginClass {
const videoBitrate = Opts.quality
this.videoRecorder.startRecording(fragmentSize, vidPath, videoBitrate, 30)

const audioPath = `${CCRecord.baseDataPath}/temp/audio${this.recordIndex}.wav`
let audioPath = `${CCRecord.baseDataPath}/temp/audio${this.recordIndex}.`
if (process.platform == 'win32') audioPath += 'mp3'
else if (process.platform == 'linux') audioPath += 'wav'
this.audioRecorder.startRecording(fragmentSize, audioPath)
}

Expand Down Expand Up @@ -156,7 +166,7 @@ export default class CCRecord implements PluginClass {
const audioPath = this.audioRecorder.audioPath

let date = new Date().toJSON()
date = date.substring(0, date.length - 5)
date = date.substring(0, date.length - 5).replace(/:/g, '-')
const outPath = `${CCRecord.baseDataPath}/video/${date}-${index}.mp4`
this.combineVideoAndAudio(this.videoRecorder.videoPath, this.audioRecorder.audioPath, outPath)
.catch(err => {
Expand Down
3 changes: 0 additions & 3 deletions src/videoencoder-polyfil.ts

This file was deleted.

0 comments on commit 9408ea1

Please sign in to comment.