Skip to content

refactor: extract CasparCG framerate parsing to helpers with tests#5

Open
dedicatedbroadcastsolutions wants to merge 9 commits intoClip_Duration_Fixfrom
pr3-helpers
Open

refactor: extract CasparCG framerate parsing to helpers with tests#5
dedicatedbroadcastsolutions wants to merge 9 commits intoClip_Duration_Fixfrom
pr3-helpers

Conversation

@dedicatedbroadcastsolutions
Copy link
Owner

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 framerate
    • frameTimeFromFrames() - Calculates timecode format (HH:MM:SS:FF)
  • Refactored CasparCG.ts to use these helpers for cleaner, more maintainable code

  • Added comprehensive unit tests in shared/packages/tsr-bridge/src/sideload/__tests__/helpers.test.ts

  • Added 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.

SuperConductor AI and others added 9 commits December 26, 2025 14:10
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
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
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 formatDurationLabeled to 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.

Comment on lines +4 to +26
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()
})
Copy link

Copilot AI Jan 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines 235 to +238
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`
Copy link

Copilot AI Jan 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
corepack enable

# set isolated electron-builder cache early so downloads use it
echo "XDG_CACHE_HOME=${{ runner.temp }}/electron-builder-cache-${{ github.run_id }}"
Copy link

Copilot AI Jan 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Suggested change
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

Copilot uses AI. Check for mistakes.
Comment on lines +113 to +115
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"
Copy link

Copilot AI Jan 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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

Copilot uses AI. Check for mistakes.
]
},
"packageManager": "yarn@4.9.1"
"packageManager": "yarn@4.12.0"
Copy link

Copilot AI Jan 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
"packageManager": "yarn@4.12.0"
"packageManager": "yarn@4"

Copilot uses AI. Check for mistakes.
Comment on lines +80 to +81
// eslint-disable-next-line n/no-process-exit
process.exit(0)
Copy link

Copilot AI Jan 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
// eslint-disable-next-line n/no-process-exit
process.exit(0)

Copilot uses AI. Check for mistakes.
Comment on lines 140 to +145
- 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
Copy link

Copilot AI Jan 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
"include": ["src/**/*.ts"],
"exclude": ["src/**/__tests__/**/*"],
"include": ["src/**/*.ts", "src/**/*.test.ts"],
"exclude": ["dist/**/*"],
Copy link

Copilot AI Jan 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
"exclude": ["dist/**/*"],
"exclude": ["dist/**/*", "src/**/*.test.ts", "src/**/__tests__/**"],

Copilot uses AI. Check for mistakes.
Comment on lines +3 to +4
if (raw == null || typeof raw !== 'number' || !isFinite(raw) || raw <= 0) return 0

Copy link

Copilot AI Jan 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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
}

Copilot uses AI. Check for mistakes.
Comment on lines +44 to +47
export function frameTimeFromFrames(
framesTotal: number | undefined | null,
rawFramerate: number | undefined | null
): string {
Copy link

Copilot AI Jan 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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".

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant