Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: linux support #86

Merged
merged 7 commits into from
Jan 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion .github/workflows/lint-and-test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,9 @@ jobs:
key: ${{ runner.os }}-modules-${{ hashFiles('**/yarn.lock') }}

- name: Prepare Environment
run: yarn
run: |
yarn config set network-timeout 100000 -g
yarn
env:
CI: true
- name: Type check
Expand All @@ -46,6 +48,7 @@ jobs:
name: Test on node ${{ matrix.node_version }} and ${{ matrix.os }}
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
node_version: ['16']
os: [ubuntu-latest, windows-latest] # [windows-latest, macOS-latest]
Expand All @@ -65,6 +68,7 @@ jobs:

- name: Prepare Environment
run: |
yarn config set network-timeout 100000 -g
yarn
yarn build
env:
Expand Down
36 changes: 22 additions & 14 deletions .github/workflows/publish-prerelease.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,26 +6,32 @@ on:

jobs:
test:
name: Test
runs-on: ubuntu-latest
timeout-minutes: 15

name: Test on node ${{ matrix.node_version }} and ${{ matrix.os }}
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
node_version: ['16']
os: [ubuntu-latest, windows-latest] # [windows-latest, macOS-latest]
timeout-minutes: 10
steps:
# Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
- uses: actions/checkout@v4
- name: Use Node.js
- name: Use Node.js ${{ matrix.node_version }}
uses: actions/setup-node@v4
with:
node-version: 16
node-version: ${{ matrix.node_version }}

- name: Prepare Environment
run: yarn
env:
CI: true
- name: Build
run: yarn build
run: |
yarn config set network-timeout 100000 -g
yarn
yarn build
env:
CI: true
- name: Run tests
run: yarn test:ci
- name: Run unit tests
run: |
yarn test:ci
env:
CI: true

Expand Down Expand Up @@ -59,7 +65,9 @@ jobs:
fi
- name: Prepare Environment
if: ${{ steps.do-publish.outputs.publish }}
run: yarn
run: |
yarn config set network-timeout 100000 -g
yarn
env:
CI: true
- name: Build
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ export function getAccessorStaticHandle(accessor: AccessorOnPackage.Any) {
} else if (accessor.type === Accessor.AccessType.HTTP_PROXY) {
return HTTPProxyAccessorHandle
} else if (accessor.type === Accessor.AccessType.FILE_SHARE) {
if (process.platform !== 'win32') throw new Error(`FileShareAccessor: not supported on ${process.platform}`)
return FileShareAccessorHandle
} else if (accessor.type === Accessor.AccessType.QUANTEL) {
return QuantelAccessorHandle
Expand Down
81 changes: 56 additions & 25 deletions shared/packages/worker/src/worker/accessorHandlers/atem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -155,12 +155,11 @@ export class ATEMAccessorHandle<Metadata> extends GenericAccessorHandle<Metadata
}

const removeListeners = () => {
atem.off('stateChanged', successHandler)
atem.off('connected', successHandler)
atem.off('error', errorHandler)
}

atem.once('stateChanged', successHandler)

atem.once('connected', successHandler)
atem.once('error', errorHandler)
})
}
Expand Down Expand Up @@ -226,9 +225,11 @@ export class ATEMAccessorHandle<Metadata> extends GenericAccessorHandle<Metadata
if (typeof bankIndex !== 'number') {
throw new Error('bankIndex is undefined')
}
let aborted = false

const streamWrapper: PutPackageHandler = new PutPackageHandler(() => {
// can't really abort the write stream
aborted = true
})
streamWrapper.usingCustomProgressEvent = true

Expand All @@ -251,10 +252,12 @@ export class ATEMAccessorHandle<Metadata> extends GenericAccessorHandle<Metadata
}
const { width, height } = info
await stream2Disk(sourceStream, inputFile)
if (aborted) return
streamWrapper.emit('progress', 0.2)

if (this.accessor.mediaType === 'still') {
await createTGASequence(inputFile, { width, height })
if (aborted) return
streamWrapper.emit('progress', 0.5)

const allTGAs = fs
Expand All @@ -272,10 +275,12 @@ export class ATEMAccessorHandle<Metadata> extends GenericAccessorHandle<Metadata

const tgaPath = allTGAs[0]
await convertFrameToRGBA(tgaPath)
if (aborted) return
streamWrapper.emit('progress', 0.7)

const rgbaPath = tgaPath.replace('.tga', '.rgba')
const rgbaBuffer = await fsReadFile(rgbaPath)
if (aborted) return
await atem.uploadStill(
bankIndex,
rgbaBuffer,
Expand All @@ -285,13 +290,15 @@ export class ATEMAccessorHandle<Metadata> extends GenericAccessorHandle<Metadata
streamWrapper.emit('progress', 1)
} else {
const duration = await countFrames(inputFile)
if (aborted) return
const maxDuration = atem.state.settings.mediaPool.maxFrames[bankIndex]
if (duration > maxDuration) {
throw new Error(`File is too long in duration (${duration} frames, max ${maxDuration})`)
}

streamWrapper.emit('progress', 0.3)
await createTGASequence(inputFile, { width, height })
if (aborted) return
streamWrapper.emit('progress', 0.4)

const allTGAs = fs
Expand All @@ -308,6 +315,7 @@ export class ATEMAccessorHandle<Metadata> extends GenericAccessorHandle<Metadata
const tga = allTGAs[index]
streamWrapper.emit('progress', 0.5 + 0.1 * (index / allTGAs.length))
await convertFrameToRGBA(tga)
if (aborted) return
}
streamWrapper.emit('progress', 0.6)

Expand All @@ -316,17 +324,29 @@ export class ATEMAccessorHandle<Metadata> extends GenericAccessorHandle<Metadata
})
const provideFrame = async function* (): AsyncGenerator<Buffer> {
for (let i = 0; i < allRGBAs.length; i++) {
if (aborted) throw new Error('Aborted')

streamWrapper.emit('progress', 0.61 + 0.29 * ((i - 0.5) / allRGBAs.length))
yield await fsReadFile(allRGBAs[i])
streamWrapper.emit('progress', 0.6 + 0.3 * (i / allRGBAs.length))
streamWrapper.emit('progress', 0.61 + 0.29 * (i / allRGBAs.length))
}
}

await atem.uploadClip(bankIndex, provideFrame(), this.getAtemClipName())
try {
await atem.uploadClip(bankIndex, provideFrame(), this.getAtemClipName())
} catch (e) {
if (`${e}`.match(/Aborted/)) {
return
} else throw e
}
if (aborted) return

const audioStreamIndicies = await getStreamIndicies(inputFile, 'audio')
if (audioStreamIndicies.length > 0) {
await convertAudio(inputFile)
await sleep(1000) // Helps avoid a lock-related "Code 5" error from the ATEM.

if (aborted) return
const audioBuffer = await fsReadFile(replaceFileExtension(inputFile, '.wav'))
await atem.uploadAudio(bankIndex, audioBuffer, `audio${this.accessor.bankIndex}`)
}
Expand Down Expand Up @@ -484,30 +504,31 @@ async function stream2Disk(sourceStream: NodeJS.ReadableStream, outputFile: stri

async function createTGASequence(inputFile: string, opts?: { width: number; height: number }): Promise<string> {
const outputFile = replaceFileExtension(inputFile, '_%04d.tga')
const args = [`-i "${inputFile}"`]
const args = ['-i', inputFile]
if (opts) {
args.push(`-vf scale=${opts.width}:${opts.height}`)
args.push('-vf', `scale=${opts.width}:${opts.height}`)
}
args.push(`"${outputFile}"`)
args.push(outputFile)

return ffmpeg(args)
}

async function convertFrameToRGBA(inputFile: string): Promise<string> {
const outputFile = replaceFileExtension(inputFile, '.rgba')
const args = [`-i "${inputFile}"`, '-pix_fmt rgba', '-f rawvideo', `"${outputFile}"`]
const args = [`-i`, inputFile, '-pix_fmt', 'rgba', '-f', 'rawvideo', outputFile]
return ffmpeg(args)
}

async function convertAudio(inputFile: string): Promise<string> {
const outputFile = replaceFileExtension(inputFile, '.wav')
const args = [
`-i "${inputFile}"`,
`-i`,
inputFile,
'-vn', // no video
'-ar 48000', // 48kHz sample rate
'-ac 2', // stereo audio
'-c:a pcm_s24le',
`"${outputFile}"`,
'-ar`,`48000', // 48kHz sample rate
'-ac`,`2', // stereo audio
'-c:a`,`pcm_s24le',
outputFile,
]

return ffmpeg(args)
Expand All @@ -516,12 +537,17 @@ async function convertAudio(inputFile: string): Promise<string> {
async function countFrames(inputFile: string): Promise<number> {
return new Promise((resolve, reject) => {
const args = [
`-i "${inputFile}"`,
'-v error',
'-select_streams v:0',
'-i',
inputFile,
'-v',
'error',
'-select_streams',
'v:0',
'-count_frames',
'-show_entries stream=nb_read_frames',
'-print_format csv',
'-show_entries',
'stream=nb_read_frames',
'-print_format',
'csv',
]

ffprobe(args)
Expand All @@ -537,11 +563,16 @@ async function countFrames(inputFile: string): Promise<number> {
async function getStreamIndicies(inputFile: string, type: 'video' | 'audio'): Promise<number[]> {
return new Promise((resolve, reject) => {
const args = [
`-i "${inputFile}"`,
'-v error',
`-select_streams ${type === 'video' ? 'v' : 'a'}`,
'-show_entries stream=index',
'-of csv=p=0',
'-i',
inputFile,
'-v',
'error',
'-select_streams',
type === 'video' ? 'v' : 'a',
'-show_entries',
'stream=index',
'-of',
'csv=p=0',
]

ffprobe(args)
Expand Down Expand Up @@ -582,7 +613,7 @@ async function ffmpeg(args: string[]): Promise<string> {
const file = getFFMpegExecutable()
execFile(
file,
['-v error', ...args],
['-v', 'error', ...args],
{
maxBuffer: MAX_EXEC_BUFFER,
windowsVerbatimArguments: true, // To fix an issue with ffmpeg.exe on Windows
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -367,18 +367,28 @@ export function previewFFMpegArguments(input: string, seekableSource: boolean, m
return [
'-hide_banner',
'-y', // Overwrite output files without asking.
seekableSource ? undefined : '-seekable 0',
`-i "${input}"`, // Input file path
'-f webm', // format: webm
seekableSource ? undefined : '-seekable',
seekableSource ? undefined : '0',
`-i`,
input, // Input file path
'-f',
'webm', // format: webm
'-an', // blocks all audio streams
'-c:v libvpx-vp9', // encoder for video (use VP9)
`-b:v ${metadata.version.bitrate || '40k'}`,
'-auto-alt-ref 1',
`-vf scale=${metadata.version.width || 320}:${metadata.version.height || -1}`, // Scale to resolution

'-threads 1', // Number of threads to use
'-cpu-used 5', // Sacrifice quality for speed, used in combination with -deadline realtime
'-deadline realtime', // Encoder speed/quality and cpu use (best, good, realtime)
'-c:v',
'libvpx-vp9', // encoder for video (use VP9)
`-b:v`,
`${metadata.version.bitrate || '40k'}`,
'-auto-alt-ref',
'1',
`-vf`,
`scale=${metadata.version.width || 320}:${metadata.version.height || -1}`, // Scale to resolution

'-threads',
'1', // Number of threads to use
'-cpu-used',
'5', // Sacrifice quality for speed, used in combination with -deadline realtime
'-deadline',
'realtime', // Encoder speed/quality and cpu use (best, good, realtime)
].filter(Boolean) as string[] // remove undefined values
}

Expand All @@ -397,15 +407,26 @@ export function thumbnailFFMpegArguments(
): string[] {
return [
'-hide_banner',
hasVideoStream && seekTimeCode ? `-ss ${seekTimeCode}` : undefined,
`-i "${input}"`,
`-f image2`,
'-frames:v 1',
hasVideoStream
? `-vf ${!seekTimeCode ? 'thumbnail,' : ''}scale=${metadata.version.width}:${metadata.version.height}` // Creates a thumbnail of the video.
: '-filter_complex "showwavespic=s=640x240:split_channels=1:colors=white"', // Creates an image of the audio waveform.
'-threads 1',
].filter(Boolean) as string[] // remove undefined values
...(hasVideoStream && seekTimeCode ? [`-ss`, `${seekTimeCode}`] : []),
`-i`,
input,
`-f`,
`image2`,
'-frames:v',
'1',
...(hasVideoStream
? [
`-vf`,
`${!seekTimeCode ? 'thumbnail,' : ''}scale=${metadata.version.width}:${metadata.version.height}`, // Creates a thumbnail of the video.
]
: [
'-filter_complex',
'showwavespic=s=640x240:split_channels=1:colors=white', // Creates an image of the audio waveform.
]),

'-threads',
'1',
]
}

/** Returns arguments for FFMpeg to generate a proxy video file */
Expand All @@ -418,10 +439,13 @@ export function proxyFFMpegArguments(
'-hide_banner',
'-y', // Overwrite output files without asking.
seekableSource ? undefined : '-seekable 0',
`-i "${input}"`, // Input file path
`-i`,
input, // Input file path

'-c copy', // Stream copy, no transcoding
'-threads 1', // Number of threads to use
'-c',
'copy', // Stream copy, no transcoding
'-threads',
'1', // Number of threads to use
]

// Check target to see if we should tell ffmpeg which format to use:
Expand Down
Loading
Loading