Skip to content

feature/encrypt-fmp4#266

Closed
cedricve wants to merge 8 commits into
masterfrom
feature/encrypt-fmp4
Closed

feature/encrypt-fmp4#266
cedricve wants to merge 8 commits into
masterfrom
feature/encrypt-fmp4

Conversation

@cedricve
Copy link
Copy Markdown
Member

@cedricve cedricve commented Apr 9, 2026

No description provided.

- Implemented ClearKeyVideo component for playing encrypted MP4 videos using Media Source Extensions.
- Created ClearKeyPlayer page to demonstrate the ClearKeyVideo component with a sample video.
- Added styles for ClearKeyVideo and ClearKeyPlayer for better layout and appearance.
- Included utility functions for parsing MP4 boxes and splitting segments.
- Integrated ClearKey license handling for video playback.
Copilot AI review requested due to automatic review settings April 9, 2026 14:33
Copy link
Copy Markdown
Contributor

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 introduces encrypted fMP4 recordings (CENC/ClearKey) end-to-end: the Go recorder writes encrypted fragments and the React UI adds ClearKey-based playback (plus a demo page) to view those recordings.

Changes:

  • Add CENC encryption support to the MP4 muxer and wire it into the recording pipeline (disabling AAC audio when encryption is enabled).
  • Replace Media page playback with a ClearKey-enabled player component and add a dedicated ClearKey demo route/page.
  • Reduce WebRTC AAC transcoder logging verbosity and adjust local dev port/debug config.

Reviewed changes

Copilot reviewed 15 out of 20 changed files in this pull request and generated 14 comments.

Show a summary per file
File Description
ui/src/pages/Media/Media.jsx Switches recordings UI to use ClearKeyVideo and fetches config to obtain the symmetric key.
ui/src/pages/ClearKeyPlayer/ClearKeyPlayer.jsx Adds a ClearKey demo player page using MSE+EME.
ui/src/pages/ClearKeyPlayer/ClearKeyPlayer.scss Styles for the ClearKey demo page.
ui/src/components/ClearKeyVideo/ClearKeyVideo.jsx Adds a reusable ClearKey-capable video component (MSE+EME + key derivation).
ui/src/components/ClearKeyVideo/ClearKeyVideo.scss Styles for the ClearKey video component.
ui/src/index.jsx Registers the /clearkey route.
ui/src/App.jsx Adds a sidebar navigation entry for the ClearKey demo.
ui/src/config.js Changes dev API/WS base URLs from port 8080 to 8089.
ui/package.json Adds video.js and videojs-contrib-eme dependencies.
machinery/src/video/mp4.go Adds segment/init encryption (CENC) and encryption configuration helpers.
machinery/src/capture/main.go Enables MP4 encryption for recordings and skips AAC audio when encrypting.
machinery/src/routers/http/Server.go Removes legacy AES decrypt-on-serve and adds a debug endpoint.
machinery/src/webrtc/main.go Lowers AAC-related logs from Info to Debug.
machinery/src/webrtc/aac_transcoder_stub.go Lowers ffmpeg/AAC transcoder logs from Info to Debug.
.vscode/launch.json Updates debug args (config dir + port 8089).
machinery/.DS_Store Adds a macOS metadata file to the repo.
Comments suppressed due to low confidence (1)

machinery/src/capture/main.go:293

  • When encryptRecordings is true, AAC audio is skipped entirely (no audio track and no audio samples). This is a user-visible behavior change (recordings lose audio) and should be made explicit (UI warning / config validation) or handled by encrypting audio too instead of dropping it.
					if audioCodec == "AAC" && !encryptRecordings {
						audioTrack = mp4Video.AddAudioTrack("AAC")
					} else if audioCodec == "PCM_MULAW" {
						log.Log.Debug("capture.main.HandleRecordStream(continuous): no AAC audio codec detected, skipping audio track.")
					}

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +85 to +89
r.Handle("GET", "/debug/recordings", func(c *gin.Context) {
c.JSON(200, gin.H{
"recordings_path": configDirectory + "/data/recordings",
})
})
Copy link

Copilot AI Apr 9, 2026

Choose a reason for hiding this comment

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

The new /debug/recordings endpoint discloses the server’s filesystem layout (recordings path) without any authentication/authorization checks. Please remove this endpoint or gate it behind admin auth and a debug/dev-only flag/build tag.

Suggested change
r.Handle("GET", "/debug/recordings", func(c *gin.Context) {
c.JSON(200, gin.H{
"recordings_path": configDirectory + "/data/recordings",
})
})
// Only expose debug routes when explicitly enabled, and protect them with authentication.
if os.Getenv("ENABLE_DEBUG_ENDPOINTS") == "true" {
debug := r.Group("/debug")
debug.Use(authMiddleware.MiddlewareFunc())
debug.GET("/recordings", func(c *gin.Context) {
c.JSON(200, gin.H{
"recordings_path": configDirectory + "/data/recordings",
})
})
}

Copilot uses AI. Check for mistakes.
Comment on lines 111 to 114
contents, err := os.ReadFile(filePath)
if err == nil {

// Get symmetric key
symmetricKey := configuration.Config.Encryption.SymmetricKey
encryptedRecordings := configuration.Config.Encryption.Recordings
// Decrypt file
if encryptedRecordings == "true" && symmetricKey != "" {

// Read file
if err != nil {
c.JSON(404, gin.H{"error": "File not found"})
return
}

// Decrypt file
contents, err = encryption.AesDecrypt(contents, symmetricKey)
if err != nil {
c.JSON(404, gin.H{"error": "File not found"})
return
}
}

// Get fileSize from contents
Copy link

Copilot AI Apr 9, 2026

Choose a reason for hiding this comment

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

This change removes server-side AES decryption when serving /file/*filepath. If there are existing recordings encrypted with the previous AES-at-rest mechanism, they will now be served as ciphertext and become unplayable/downloadable. Consider supporting both formats during a transition (e.g., detect legacy format/header and decrypt) or clearly version/migrate recordings.

Copilot uses AI. Check for mistakes.
Comment on lines +894 to +903
psshBoxes, err := buildPsshBoxes(kidUUID)
if err != nil {
log.Log.Error("mp4.ConfigureEncryption(): invalid PSSH system id: " + err.Error())
}

mp4.EncryptRecordings = true
mp4.EncryptionKey = key
mp4.EncryptionIV = iv
mp4.EncryptionKID = kidUUID
mp4.EncryptionPssh = psshBoxes
Copy link

Copilot AI Apr 9, 2026

Choose a reason for hiding this comment

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

If buildPsshBoxes fails, encryption is still enabled and mp4.EncryptionPssh may be nil. That can lead to EncryptFragment/InitProtect failures later or produce encrypted output missing required init data. Please return early on this error and do not enable mp4.EncryptRecordings unless all encryption material (KID, key, IV, PSSH) is successfully initialized.

Copilot uses AI. Check for mistakes.
Comment thread machinery/src/video/mp4.go Outdated
Comment on lines +830 to +846
usedFallback = true
sum := sha256.Sum256([]byte(sym))
key = append([]byte(nil), sum[:16]...)
kid = append([]byte(nil), sum[16:32]...)
ivSum := sha256.Sum256(append([]byte("iv:"), []byte(sym)...))
iv = append([]byte(nil), ivSum[:16]...)
return key, kid, iv, usedFallback, nil
}

sum := sha256.Sum256(key)
kid = append([]byte(nil), sum[:16]...)
ivSeed := make([]byte, 0, 3+len(key))
ivSeed = append(ivSeed, []byte("iv:")...)
ivSeed = append(ivSeed, key...)
ivSum := sha256.Sum256(ivSeed)
iv = append([]byte(nil), ivSum[:16]...)
return key, kid, iv, false, nil
Copy link

Copilot AI Apr 9, 2026

Choose a reason for hiding this comment

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

deriveCencMaterial deterministically derives the CENC IV from the symmetric key. With AES-CTR (cenc), reusing IVs with the same key across recordings can leak information. Prefer generating a cryptographically-random per-recording IV (crypto/rand) and letting the IVs be carried in the file’s senc data, rather than deriving a constant IV from the key.

Copilot uses AI. Check for mistakes.
Comment on lines +319 to 327
if mp4.EncryptRecordings {
if encErr := mp4.encryptSegment(mp4.Segment); encErr != nil {
log.Log.Error("mp4.AddSampleToTrack(): encrypt segment failed: " + encErr.Error())
}
}
if segErr := mp4.Segment.Encode(mp4.Writer); segErr != nil {
log.Log.Error("mp4.AddSampleToTrack(): error encoding segment: " + segErr.Error())
}
mp4.Segments = append(mp4.Segments, mp4.Segment)
Copy link

Copilot AI Apr 9, 2026

Choose a reason for hiding this comment

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

When encryption is enabled, an encryptSegment failure is logged but the segment is still encoded and written. That can produce a file with mixed encrypted/clear fragments and/or a file that the player cannot decode. Please fail the recording (return error to caller) or disable writing the segment when encryption is expected but encryption fails.

Suggested change
if mp4.EncryptRecordings {
if encErr := mp4.encryptSegment(mp4.Segment); encErr != nil {
log.Log.Error("mp4.AddSampleToTrack(): encrypt segment failed: " + encErr.Error())
}
}
if segErr := mp4.Segment.Encode(mp4.Writer); segErr != nil {
log.Log.Error("mp4.AddSampleToTrack(): error encoding segment: " + segErr.Error())
}
mp4.Segments = append(mp4.Segments, mp4.Segment)
encryptionSucceeded := true
if mp4.EncryptRecordings {
if encErr := mp4.encryptSegment(mp4.Segment); encErr != nil {
encryptionSucceeded = false
log.Log.Error("mp4.AddSampleToTrack(): encrypt segment failed: " + encErr.Error())
}
}
if encryptionSucceeded {
if segErr := mp4.Segment.Encode(mp4.Writer); segErr != nil {
log.Log.Error("mp4.AddSampleToTrack(): error encoding segment: " + segErr.Error())
} else {
mp4.Segments = append(mp4.Segments, mp4.Segment)
}
}

Copilot uses AI. Check for mistakes.
Comment on lines +230 to +253
const handleEncrypted = async (event) => {
await mediaKeysReady;
if (!video.mediaKeys) {
return;
}
const resolved = await ensureKeyMaterial();
if (!resolved.kid || !resolved.key) {
return;
}
const session = video.mediaKeys.createSession();
session.addEventListener('message', async () => {
const resolved = await ensureKeyMaterial();
if (!resolved.kid || !resolved.key) {
return;
}
const license = JSON.stringify({
keys: [{ kty: 'oct', kid: resolved.kid, k: resolved.key }],
type: 'temporary',
});
const licenseBytes = new TextEncoder().encode(license);
await session.update(licenseBytes);
});
await session.generateRequest(event.initDataType, event.initData);
};
Copy link

Copilot AI Apr 9, 2026

Choose a reason for hiding this comment

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

The async EME flow doesn’t catch errors from session.generateRequest() / session.update() in ClearKeyVideo. If these reject (unsupported initData, bad license JSON, etc.), it can create unhandled promise rejections and leave the player stuck. Wrap these awaits in try/catch and surface a user-visible error state.

Copilot uses AI. Check for mistakes.
Comment on lines +284 to +290
mediaSource.addEventListener('sourceopen', async () => {
try {
await setupMediaKeys();
const response = await fetch(src);
const arrayBuffer = await response.arrayBuffer();
const { initSegment, segments } = splitFmp4(arrayBuffer);

Copy link

Copilot AI Apr 9, 2026

Choose a reason for hiding this comment

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

Fetch/MSE setup loads the entire src into an ArrayBuffer and appends all segments. This is memory/bandwidth heavy for real recordings and cannot be cancelled if the component unmounts mid-download. Consider using an AbortController for fetch, check response.ok, and/or implement incremental range/streaming appends rather than downloading the full file up front.

Copilot uses AI. Check for mistakes.
Comment thread ui/src/components/ClearKeyVideo/ClearKeyVideo.scss
Comment thread ui/package.json Outdated
Comment thread ui/src/App.jsx
cedricve and others added 3 commits April 9, 2026 16:52
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
@cedricve cedricve closed this May 4, 2026
@cedricve cedricve deleted the feature/encrypt-fmp4 branch May 4, 2026 18:13
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.

2 participants