refactor: extract CasparCG framerate parsing to helpers with tests#5
refactor: extract CasparCG framerate parsing to helpers with tests#5dedicatedbroadcastsolutions wants to merge 9 commits intoClip_Duration_Fixfrom
Conversation
fix(tsr-bridge): use explicit .js ESM import for helpers ci: use upstream Node CI workflow (node.yaml) chore(tsr-bridge): fix lint/prettier issues; include tests in package tsconfigs; remove unused import chore: remove non-PR files and revert temp test script; keep only casparcg fix + helpers + tests tests: add .js extensions & include jest types in tsconfigs to fix typecheck tests: fix type-check (add input mock for OBS input types) lint: fix Prettier reflows and add ESLint exceptions for fetch usage; fix test indentation lint: fix Prettier indentation and helper signatures; align telemetry ESLint comment fix: correct logic/formatting in CasparCG and telemetry; Prettier conformances style(tsr-bridge): align indentation for Prettier fix(tsr-bridge): restore resource object (lost during formatting) Fix Prettier indentation in CasparCG and telemetry Restore missing resource fields (id, type, name) in CasparCG resource
…rce locator, formatDurationLabeled)
ci: refresh workflows (push refresh) style: fix Prettier whitespace in timeLib.ts ci: run electron-builder per workspace to fix macOS packaging ci: set GH_TOKEN to GITHUB_TOKEN for binary builds ci: add GH_TOKEN & isolated electron-builder cache, retries, and defensive artifact moves ci(windows): run build:binary under bash so retry helper works ci(macos): make notarize defensive and add debug logs for missing app path ci(lint): allow require() for fs in notarize.cjs to satisfy linter ci: add prep-build-binary wrapper to log expected paths before electron-builder (linted) ci: use CommonJS prep-build-binary wrapper for tsr-bridge chore: remove ESM prep-build-binary script fix(tsr-bridge): prep-build-binary lint/prettier (remove shebang, format tabs) ci(macos): add pre-build diagnostics, clear builder cache, upload builder configs
scripts(tsr-bridge): make prep-build-binary robust to missing npx and avoid throwing scripts(tsr-bridge): robust shell fallback for electron-builder (npx→yarn→local) Update packageManager to yarn@4.12.0
…onsole, catch binding) fix(tsr-bridge): clean electron-output before binary build and ensure electron-builder availability ci: set XDG_CACHE_HOME early in macOS Prepare Environment step fix(tsr-bridge): use yarn exec for electron-builder in Yarn workspaces
fix(ci): correct secrets conditional syntax in workflow fix(ci): use proper secrets syntax in if condition fix(ci): use environment variable for secrets check in if condition
210b443 to
ab1c917
Compare
There was a problem hiding this comment.
Pull request overview
This PR extracts CasparCG framerate parsing logic into reusable helper functions and adds tests, but also includes substantial CI/build infrastructure improvements that were described as being separated into a different PR.
Key changes:
- Extracted framerate parsing and duration calculation into three helper functions with unit tests
- Modified
formatDurationLabeledto output full milliseconds instead of decimal seconds - Added extensive CI improvements including retry logic, conditional builds, and diagnostic logging
Reviewed changes
Copilot reviewed 17 out of 17 changed files in this pull request and generated 10 comments.
Show a summary per file
| File | Description |
|---|---|
| shared/packages/tsr-bridge/src/sideload/helpers.ts | New helper functions for CasparCG framerate parsing, duration calculation, and timecode formatting |
| shared/packages/tsr-bridge/src/sideload/tests/helpers.test.ts | Basic unit tests for the new helper functions |
| shared/packages/tsr-bridge/src/sideload/CasparCG.ts | Refactored to use new helper functions, removing inline framerate parsing logic |
| shared/packages/tsr-bridge/src/sideload/CasparCGTemplates.ts | Added eslint disable comment for fetch usage |
| shared/packages/tsr-bridge/tsconfig.json | Updated to include test files and add Jest types |
| shared/packages/lib/src/Resources.ts | Modified resource locator logic for OBS input types |
| apps/app/src/lib/timeLib.ts | Changed formatDurationLabeled to output full milliseconds and added edge case handling |
| apps/app/src/tests/timeLib.test.ts | New basic test file for timeLib formatting |
| apps/app/src/lib/tests/resources.test.ts | Added input property to OBS resource test fixtures |
| apps/app/src/electron/telemetry.ts | Added eslint disable comment for fetch usage |
| apps/app/tsconfig.build.json | Updated to include test files and add Jest types |
| jest.config.base.cjs | Added workspace module resolution mapper |
| package.json | Updated package manager from yarn@4.9.1 to yarn@4.12.0 |
| apps/tsr-bridge/package.json | Modified build:binary script to use new prep script |
| apps/tsr-bridge/scripts/prep-build-binary.cjs | New script with diagnostic logging and fallback electron-builder execution |
| apps/tsr-bridge/tools/notarize.cjs | Enhanced with defensive checks, logging, and error handling |
| .github/workflows/node.yaml | Extensive CI improvements including retry logic, conditional builds, isolated caches, and diagnostic artifact collection |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| test('parse simple fps', () => { | ||
| expect(parseCasparFramerate(30)).toBe(30) | ||
| expect(parseCasparFramerate(60)).toBe(60) | ||
| }) | ||
|
|
||
| test('parse fps*1000', () => { | ||
| expect(parseCasparFramerate(30000)).toBeCloseTo(30) | ||
| expect(parseCasparFramerate(59940)).toBeCloseTo(59.94) | ||
| }) | ||
|
|
||
| test('parse fps*1001 (ntsc)', () => { | ||
| expect(parseCasparFramerate(30030)).toBeCloseTo(29.97, 2) | ||
| }) | ||
|
|
||
| test('duration from frames', () => { | ||
| expect(durationFromFrames(300, 30000)).toBeCloseTo(10) | ||
| expect(durationFromFrames(2997, 30030)).toBeGreaterThan(49) | ||
| }) | ||
|
|
||
| test('frameTime from frames', () => { | ||
| const ft = frameTimeFromFrames(3601 * 30 + 5, 30) | ||
| expect(ft.startsWith('01:00:01')).toBeTruthy() | ||
| }) |
There was a problem hiding this comment.
The test suite lacks comprehensive coverage of edge cases for the helper functions. Consider adding tests for:
- Invalid inputs (null, undefined, 0, negative values, NaN, Infinity)
- Edge cases in parseCasparFramerate: values between 70-1000, values outside typical ranges
- Edge cases in durationFromFrames: exceeding the 86400 second limit, negative frame counts
- Edge cases in frameTimeFromFrames: very large frame counts, negative values
Without these tests, it's difficult to verify that the helper functions correctly handle all edge cases that the original inline code handled.
| if (secondTenths) { | ||
| returnStr += `${s}.${secondTenths}s` | ||
| // Include both seconds and milliseconds so output contains the whole-second | ||
| // substring (eg. "1s500ms"), which tests expect. | ||
| returnStr += `${s}s${ms}ms` |
There was a problem hiding this comment.
The change from formatting seconds with tenths (e.g., "1.5s") to including both seconds and full milliseconds (e.g., "1s500ms") significantly changes the output format. This could break existing code or UI expectations that rely on the previous compact format. While the comment mentions this is "which tests expect," this appears to be changing the function behavior to match the tests rather than the other way around. Consider whether this breaking change in output format is intentional and document it if so.
| corepack enable | ||
|
|
||
| # set isolated electron-builder cache early so downloads use it | ||
| echo "XDG_CACHE_HOME=${{ runner.temp }}/electron-builder-cache-${{ github.run_id }}" |
There was a problem hiding this comment.
The environment variable is echoed but not exported to subsequent steps. Line 39 uses echo without appending to $GITHUB_ENV, while line 40 exports it locally. To make this variable available to subsequent steps in the workflow, you should use: echo "XDG_CACHE_HOME=${{ runner.temp }}/electron-builder-cache-${{ github.run_id }}" >> $GITHUB_ENV
| echo "XDG_CACHE_HOME=${{ runner.temp }}/electron-builder-cache-${{ github.run_id }}" | |
| echo "XDG_CACHE_HOME=${{ runner.temp }}/electron-builder-cache-${{ github.run_id }}" >> $GITHUB_ENV |
| retry() { cmd="$1"; n=0; until [ $n -ge 3 ]; do eval "$cmd" && return 0; n=$((n+1)); echo "Retry $n for: $cmd"; sleep 5; done; return 1; } | ||
| retry "yarn workspace tsr-bridge build:binary -- --publish=never" | ||
| retry "yarn workspace superconductor build:binary -- --publish=never" |
There was a problem hiding this comment.
The retry function uses eval which can be risky if command arguments contain special characters. While the current usage appears safe, consider using a more robust approach. Additionally, the retry logic sleeps for only 5 seconds between attempts which may be insufficient for transient network issues with electron-builder downloads. Consider increasing the sleep duration or using exponential backoff.
| retry() { cmd="$1"; n=0; until [ $n -ge 3 ]; do eval "$cmd" && return 0; n=$((n+1)); echo "Retry $n for: $cmd"; sleep 5; done; return 1; } | |
| retry "yarn workspace tsr-bridge build:binary -- --publish=never" | |
| retry "yarn workspace superconductor build:binary -- --publish=never" | |
| retry() { | |
| n=0 | |
| delay=5 | |
| while [ $n -lt 3 ]; do | |
| "$@" && return 0 | |
| n=$((n+1)) | |
| echo "Retry $n for: $*" | |
| sleep "$delay" | |
| delay=$((delay * 2)) | |
| done | |
| return 1 | |
| } | |
| retry yarn workspace tsr-bridge build:binary -- --publish=never | |
| retry yarn workspace superconductor build:binary -- --publish=never |
| ] | ||
| }, | ||
| "packageManager": "yarn@4.9.1" | ||
| "packageManager": "yarn@4.12.0" |
There was a problem hiding this comment.
The package manager version has been updated from yarn@4.9.1 to yarn@4.12.0. While this is a minor version bump within the same major version, ensure that all developers and CI environments update their yarn version accordingly. This change could cause inconsistencies if not applied uniformly across the team and CI.
| "packageManager": "yarn@4.12.0" | |
| "packageManager": "yarn@4" |
| // eslint-disable-next-line n/no-process-exit | ||
| process.exit(0) |
There was a problem hiding this comment.
This PR description states it "separates the core refactoring from CI/infrastructure changes" but includes extensive CI/build infrastructure modifications (GitHub workflow changes, new prep-build-binary.cjs script, notarize.cjs updates, package manager version bump). Consider splitting these into separate PRs as originally intended - one for the CasparCG helper refactoring and tests, and another for CI/build improvements. This would make the changes easier to review, test, and potentially revert if needed.
| // eslint-disable-next-line n/no-process-exit | |
| process.exit(0) |
| - name: Collect binaries | ||
| run: | | ||
| mkdir macos-dist | ||
| mv apps/tsr-bridge/electron-output/TSR-Bridge* macos-dist/ | ||
| mv apps/app/electron-output/SuperConductor* macos-dist/ | ||
| mv apps/app/electron-output/latest-mac.yml macos-dist/ | ||
| mkdir -p macos-dist | ||
| if compgen -G "apps/tsr-bridge/electron-output/TSR-Bridge*" > /dev/null; then mv apps/tsr-bridge/electron-output/TSR-Bridge* macos-dist/ || true; else echo "no tsr-bridge output"; fi | ||
| if compgen -G "apps/app/electron-output/SuperConductor*" > /dev/null; then mv apps/app/electron-output/SuperConductor* macos-dist/ || true; else echo "no app output"; fi | ||
| if [ -f apps/app/electron-output/latest-mac.yml ]; then mv apps/app/electron-output/latest-mac.yml macos-dist/ || true; else echo "no latest-mac.yml"; fi |
There was a problem hiding this comment.
The "Build binaries" step (line 109-123) is conditionally skipped when MAC_CSC_LINK secret is not available, but the "Collect binaries" step (line 140-145) runs unconditionally. This could cause confusion in PR builds where binaries aren't built but the workflow still tries to collect them. Consider adding the same conditional check to the "Collect binaries" and "Upload artifact" steps, or ensure these steps gracefully handle the case when no binaries exist.
| "include": ["src/**/*.ts"], | ||
| "exclude": ["src/**/__tests__/**/*"], | ||
| "include": ["src/**/*.ts", "src/**/*.test.ts"], | ||
| "exclude": ["dist/**/*"], |
There was a problem hiding this comment.
The tsconfig now includes test files in the compilation by adding "src/**/*.test.ts" to the include array and removing the exclude pattern for tests. While this is generally fine for Jest tests, ensure that test files don't get bundled into production builds. Verify that your build pipeline properly excludes test files from the final output.
| "exclude": ["dist/**/*"], | |
| "exclude": ["dist/**/*", "src/**/*.test.ts", "src/**/__tests__/**"], |
| if (raw == null || typeof raw !== 'number' || !isFinite(raw) || raw <= 0) return 0 | ||
|
|
There was a problem hiding this comment.
The parseCasparFramerate function returns 0 for invalid inputs, but this could mask actual errors where framerate data is corrupt or malformed. The original code in CasparCG.ts had explicit logging for these cases, which has been removed. Consider adding error logging within the helper function or ensuring callers log appropriately when receiving a 0 return value, so debugging production issues remains feasible.
| if (raw == null || typeof raw !== 'number' || !isFinite(raw) || raw <= 0) return 0 | |
| if (raw == null) return 0 | |
| if (typeof raw !== 'number' || !isFinite(raw) || raw <= 0) { | |
| console.warn('[parseCasparFramerate] Invalid framerate value received:', raw) | |
| return 0 | |
| } |
| export function frameTimeFromFrames( | ||
| framesTotal: number | undefined | null, | ||
| rawFramerate: number | undefined | null | ||
| ): string { |
There was a problem hiding this comment.
The function is named frameTimeFromFrames but the comment in helpers.ts line 59 shows it returns a "timecode format (HH:MM:SS:FF)". Consider renaming to timecodeFromFrames or formatFramesAsTimecode for clarity, as "frameTime" could be confused with "time per frame" (frame duration) rather than "timecode representation of frames".
This PR extracts the framerate parsing and duration calculation logic into reusable helper functions with comprehensive unit tests.
Changes
Created new helper functions in
shared/packages/tsr-bridge/src/sideload/helpers.ts:parseCasparFramerate()- Handles CasparCG framerate formats (fps, fps1000, fps1001)durationFromFrames()- Calculates duration from frames and framerateframeTimeFromFrames()- Calculates timecode format (HH:MM:SS:FF)Refactored
CasparCG.tsto use these helpers for cleaner, more maintainable codeAdded comprehensive unit tests in
shared/packages/tsr-bridge/src/sideload/__tests__/helpers.test.tsAdded test file for timeLib formatting
Fixed Jest configuration for workspace resolution
This separates the core refactoring from CI/infrastructure changes, making it easier to review and merge independently.