Skip to content

feat(obs): carry a video's length through the E2EE upload - #240

Merged
EdamAme-x merged 2 commits into
evex-dev:mainfrom
frankekn:upstream/video-duration
Sep 7, 2026
Merged

EdamAme-x merged 2 commits into
evex-dev:mainfrom
frankekn:upstream/video-duration

Conversation

@frankekn

@frankekn frankekn commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

Summary

A video uploaded over E2EE arrives on LINE clients with no length — the bubble renders as a 0:00 clip — while the same file sent through the plain upload path shows its real duration. uploadMediaByE2EE has no way to carry one.

The two paths differ in who builds the message. uploadObjTalk (packages/linejs/base/obs/mod.ts:177-237) never builds one: it posts the object with oid: "reqseq" and lets the server create the message from the obs params, and it already sends the length as an obs param —

} else if (type === "audio" || type === "video") {
	param.duration = (durationMs ?? 1919).toString();
}

(:221-225), which SquareChat#sendVideo / #sendAudio thread through (packages/linejs/client/features/square/mod.ts:131-164).

uploadMediaByE2EE builds the message's contentMetadata itself (packages/linejs/base/obs/mod.ts:415-437) and that literal has no duration key at all:

contentMetadata: {
	SID: obsNamespace,
	OID: objId,
	FILE_SIZE: edata.size.toString(),
	e2eeVersion: "2",
	...(oType === "image" || oType === "gif" || oType === "video")
		? { MEDIA_CONTENT_INFO: JSON.stringify({ /* ... */ }) }
		: {},
},

Nothing downstream can fill it in, either: on this path obs only ever receives the encrypted blob (:368-380 uploads encryptedData), so there is no container for the server to read a length out of the way there is for a plain upload. The value has to travel in contentMetadata.

Fix. An optional durationMs on the options object, spread into contentMetadata as DURATION when — and only when — oType === "video":

const durationMetadata: Record<string, string> =
	oType === "video" && durationMs !== undefined &&
		Number.isFinite(durationMs) && durationMs > 0
		? { DURATION: Math.round(durationMs).toString() }
		: {};

Additive throughout: the option is optional, every existing call site behaves exactly as it did, and packages/types needs nothing — contentMetadata is a Record<string, string> on the wire and already accepts the key. 0, a negative, NaN and Infinity are dropped rather than sent as garbage, since a bogus length renders worse than none.

Two details worth flagging for review:

  • The Record<string, string> annotation on durationMetadata is load-bearing, not decoration. Without it TS widens the ternary to { DURATION: string } | { DURATION?: undefined }, and the second member is not assignable to the index signature on sendMessage's contentMetadata (packages/linejs/base/service/talk/mod.ts:96) — deno check fails with TS2322.
  • DURATION is the key a LINE video message carries for its length, in milliseconds. It cannot be cross-checked inside this repository: nothing in packages/types models contentMetadata, which is an untyped string map on the wire (grep -rn DURATION packages/ returns nothing outside this change and the unrelated param.duration above). If you know the key by another name, it is one string to change.

Audio is deliberately left out. For the same reason: the tree gives no evidence for what key a voice message uses, and DURATION for audio would be a guess written into the protocol layer. oType === "audio" is therefore untouched and still sends no length over E2EE. If you can confirm the voice key, it is a one-token change to that guard plus one test case — happy to add it here or as a follow-up, whichever you prefer.

Testing

Three cases added to packages/linejs/base/obs/upload_e2ee.test.ts, built on that file's existing fakeClient() / makeObs() fixtures plus a small sentMetadata() helper that asserts exactly one talk.sendMessage and returns its contentMetadata:

  1. A video with durationMs: 4200.4 → DURATION === "4200" (milliseconds, rounded).
  2. A video with no durationMs → no DURATION; and an image with durationMs: 4200 → no DURATION either, covering the video-only guard.
  3. 0, -1, NaN and Infinity on a video → no DURATION.

Case 1 doubles as the negative control: commenting the ...durationMetadata spread back out fails it (5 passed | 1 failed) and leaves the other five obs cases green, so it is testing the new line rather than the fixture.

cd packages/linejs && deno test -A — 303 passed, 0 failed, twice in a row. deno fmt --check and deno lint on both touched files, and deno check packages/linejs/client/mod.ts, are clean.

client/features/call/stun.test.ts needs a live UDP round trip to stun.l.google.com; it passed on both runs here, but it has flaked on this machine before and is untouched by this change.

uploadMediaByE2EE builds the message's contentMetadata itself and had no
way to carry a duration, so an E2EE video went out with no DURATION and
LINE clients rendered it as a 0:00 clip. On this path obs only ever sees
the encrypted blob, so it cannot read the length out of the container the
way it does for a plain uploadObjTalk (which already sends an obs
"duration" param); the value has to travel in contentMetadata instead.

Video only: nothing in the thrift types says LINE reads the same key for a
voice message, so audio is left alone until that is confirmed. A
non-finite or non-positive duration is dropped rather than sent as garbage.

(cherry picked from commit a041d4a)
Copilot AI lite review requested due to automatic review settings September 7, 2026 08:48

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Changes recommended

The new DURATION guard can still emit "0" when durationMs is a small positive value that rounds to zero, which undermines the intended “drop non-positive durations” behavior.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

This PR fixes missing video duration metadata for the E2EE upload path in @evex/linejs by adding an optional durationMs parameter to LineObs.uploadMediaByE2EE and threading it into the outgoing talk.sendMessage contentMetadata as DURATION for videos, with accompanying unit tests.

Changes:

  • Add durationMs?: number to uploadMediaByE2EE options and include a guarded DURATION entry in contentMetadata for oType === "video".
  • Add tests asserting DURATION is set for videos with a valid duration, and absent otherwise (omitted, non-video, non-finite, or non-positive).
File summaries
File Description
packages/linejs/base/obs/mod.ts Adds optional durationMs and spreads DURATION into E2EE message contentMetadata for videos.
packages/linejs/base/obs/upload_e2ee.test.ts Adds coverage to verify DURATION behavior across valid/invalid/omitted duration inputs and non-video types.
Review details
  • Files reviewed: 2/2 changed files
  • Comments generated: 1
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread packages/linejs/base/obs/mod.ts
@EdamAme-x

Copy link
Copy Markdown
Member

Reviewed both changed files and the adjacent upload/encryption/sendMessage paths. Follow-up 2bbc4a7 fixes the rounded-zero bug (also noted by Copilot): 0.1ms previously emitted DURATION="0". Validation now runs on the rounded positive safe integer, also excluding unsafe/scientific-notation values. Added four test groups for boundaries, non-video metadata parity (audio/file/image/GIF), and video preview key reuse plus complete message metadata.

Validation: deno test -A = 351 passed / 0 failed; touched-file deno lint and fmt passed; linejs and linejs-types JSR dry-runs passed. Existing Node transport test failed locally twice: RPC cancellation produced TypeError after ~31s instead of AbortError, while PUSH passed. The test and its runtime import (base/core/node_fetch.ts) are unchanged from main. Waiting for updated CI, including its Node transport step, before merge.

Protocol evidence is limited: LINE public Messaging API documentation uses milliseconds for received video duration (https://developers.line.biz/en/reference/messaging-api/nojs/), and existing independent client code uses DURATION for video (https://github.com/fadhiilrachman/line-py/blob/master/linepy/talk.py). This is corroboration, not a live E2EE compatibility test. No live LINE account was used; audio remains intentionally unchanged. The option is caller-supplied and does not infer duration from the container. Target patch release: v3.4.2.

@EdamAme-x
EdamAme-x merged commit ef6c3d9 into evex-dev:main Sep 7, 2026
2 checks passed
@frankekn

frankekn commented Sep 7, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for pushing 2bbc4a7 onto the branch — adopted as-is, nothing of mine on top. Number.isSafeInteger on the rounded value is the better guard: it catches the 0.4 → "0" case Copilot flagged and also keeps 1e21 from going out as DURATION: "1e+21", which the raw-value check would have let through.

Verified locally on that commit: deno fmt --check and deno lint on both touched files clean, deno check packages/linejs/client/mod.ts clean, and cd packages/linejs && deno test -A → 307 passed, 0 failed (the obs file is now 10 cases).

One thing from the description still open, whenever convenient: audio. The E2EE path still sends no length for a voice message, because nothing in the tree confirms the key name for one. If DURATION is right for audio as well, it is one token in the oType === "video" guard plus a test case — happy to add it here or leave it for a follow-up, whichever you prefer.

@frankekn

frankekn commented Sep 7, 2026

Copy link
Copy Markdown
Contributor Author

Crossed with your comment — the audio question is answered, thanks. The Messaging API duration units plus line-py using DURATION for video is exactly the corroboration the description was missing, and leaving voice out until an E2EE capture settles its key is the right call.

@frankekn
frankekn deleted the upstream/video-duration branch September 9, 2026 02:36
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.

3 participants