CoWatcher Architecture Overview
CoWatcher is a high-performance, modular browser extension designed for dual subtitles, real-time AI translation, vocabulary learning, and language immersion across modern streaming platforms (Netflix, TVer, YouTube, Disney+, etc.). It natively targets Chrome (Manifest V3) and Firefox (Manifest V3) from a single, unified TypeScript codebase.
1. Core Engineering Tenets & Architectural Rules
1. Explicit over Implicit
- We discourage
anyorobjectbut that doesn't mean you cannot use it. If possible, you should specify a carefully defined data type instead of some guessing about object, array, or whatever. - If your data type is shared with more than one class, or in other words, if it is not some internal using only data type, you should define it in a domain dir in its related scope. The scopes means the root dir like
content,injected,background, and so on. If such data type is shared across scopes, it should be defined undershared's domain.
We don't want anyone to guess other's work or purpose. An explicit defined data type means an explicit function with explicit parameters and return types. It will make following development easier and more maintainable.
2. No Silent Fallbacks (Fail-Fast Principle)
In other languages, you might hear fallback as some advanced techniques, but it is not the case here. In js/ts, there are too many fallbacks including guessing a data type and write at least three fallbacks to handle it, which is just bullshit and meaningless! In our business logic(You can understand this as any codes excepts adapter related codes), fallback is discouraged! Just let this program crash and throw error! We prefer error message or stack trace to tell us which is wrong rather than silently handling it. You declare you only accept specific carefully defined data types, then, if your down-stream uses a wrong data type, just let them shoot their own feet!
ADAPTERs are exceptional. You can write as many fallback as you like for adapters as long as it works! But in practice, a carefully design is more important than fallbacks.
3. Strictly No Anemic DTOs
IMPORTANT
Data Transfer Objects (DTOs) are NOT allowed anywhere in this repository.
Currently, we disallows any DTOs to exist in this project.
Indeed, we use message to communicate between different browser components, but those are all our own logic. We don't have split brain problem and should never have.
4. Platform Anti-Corruption Layer (ACL)
- Third-party streaming platforms (Netflix, TVer, YouTube, Disney+, etc.) have volatile DOMs and proprietary player engines.
- Quarantine platform specifics: Keep all proprietary APIs (Kadmium player, VideoJS, YouTube ytplayer), selectors, and raw subtitle format parsers inside
src/injected/infrastructure/adapters/<platform>/andsrc/content/infrastructure/adapters/<platform>/. If you need new interface, define it in related adapter interfaces, just remember help existing adapters to implement it! - The core domain (
SubtitleManager,SubtitleOverlay,TranslationPipelineService) must stay completely platform-agnostic.
5. Packaging
package.jsonis the sole authority for extensionversion,displayName, anddescription. Webpack injects these dynamically into both Chrome and Firefox manifests during compilation.
2. Multi-World System Topology
Browser extensions operate across isolated execution boundaries. CoWatcher partitions its responsibilities across three distinct execution contexts:
flowchart TB
subgraph WebPageContext["Page Execution Context (MAIN World)"]
direction TB
PagePlayer["Streaming Video Player\n(Kadmium / VideoJS / HTML5 Video)"]
InjectedHooks["Network Hooks & API Interceptors\n(window.fetch / XMLHttpRequest)"]
InjectedAdapter["IInjectedAdapter\n(Netflix / TVer / YouTube)"]
InjectedAdapterRegistry["InjectedAdapterRegistry"]
end
subgraph WindowBridge["Window Messaging Bridge"]
PostMessage["window.postMessage\n(NCW_MESSAGE_TYPES Contract)"]
end
subgraph ContentScriptContext["Extension Context (ISOLATED World)"]
direction TB
ContentAdapter["IStreamingAdapter\n(Netflix / TVer / YouTube)"]
SubtitleParser["SubtitleParser\n(WebVTT / TTML / XML)"]
SubtitleManager["SubtitleManager\n(Timeline Sync & Cue Lifecycle)"]
AdDetector["IAdDetector\n(Ad Muting & State Tracking)"]
SubtitleOverlay["SubtitleOverlay\n(Multi-Slot Player DOM Mounting)"]
end
subgraph BackgroundContext["Background Service Worker / Event Page"]
direction TB
Dispatcher["Background Message Dispatcher"]
TranslationPipeline["TranslationPipelineService\n(Batching & LLM Supervision)"]
LLMAdapters["LLM Infrastructure\n(OpenRouter / Ollama)"]
GlossaryService["GlossaryService & Context Warmup"]
VocabService["VocabService & SubtitleLearning"]
CacheStorage["TrackCacheStore\n(IndexedDB / chrome.storage)"]
end
subgraph PresentationContext["Extension UI Pages"]
Popup["Popup UI\n(Provider / Target Lang / Quick Toggle / Other Configurations)"]
Options["Options UI\n(Subtitles/Contexts/Vocabulary/Watching Record management)"]
Editor["Editor UI\n(Glossary editing)"]
end
PagePlayer -. Network Responses .-> InjectedHooks
InjectedHooks --> InjectedAdapter
InjectedAdapterRegistry --> InjectedAdapter
InjectedAdapter -->|NCW_SUBTITLE_RAW_DATA\nNCW_METADATA_LOADED| PostMessage
PostMessage --> ContentAdapter
ContentAdapter --> SubtitleParser
SubtitleParser --> SubtitleManager
SubtitleManager --> SubtitleOverlay
AdDetector --> SubtitleOverlay
ContentScriptContext <==>|chrome.runtime.sendMessage| Dispatcher
PresentationContext <==>|chrome.runtime.sendMessage| Dispatcher
Dispatcher --> TranslationPipeline
TranslationPipeline --> LLMAdapters
Dispatcher --> GlossaryService
Dispatcher --> VocabService
Dispatcher --> CacheStorageExecution World Breakdown
| World / Boundary | Scope & Permissions | Core Responsibilities |
|---|---|---|
injected/(Execution World: MAIN) | Full access to webpage window, global variables, prototype monkey-patching. No extension APIs. | Intercepts streaming manifest requests, raw subtitle downloads, and proprietary player APIs (Netflix Kadmium, YouTube player). Dispatches standardized event payloads via window.postMessage. |
content/(Execution World: ISOLATED) | Full DOM access; isolated JS sandbox; access to chrome.storage and chrome.runtime. | Receives bridge messages from injected/, normalizes subtitle cues, locates player presentation containers, renders custom bilingual overlays, manages playhead synchronization and ad hiding. |
background/(Service Worker in Chrome, Event Page in Firefox) | Background script with full extension privileges, network capabilities, and persistent IndexedDB storage. | Coordinates the AI translation pipeline, streams LLM completions (OpenRouter / Ollama), manages terminology glossaries, context memory, and cached tracks. |
popup/, options/, editor/(Extension Pages) | Standard extension UI HTML/CSS/TS. | Configuration, API key management, prompt engineering, vocabulary browsing, and manual track corrections. |
3. Domain-Driven Design (DDD) Layering
CoWatcher structures code strictly according to Domain-Driven Design principles:
src/
├── shared/ # Shared kernel across all layers
│ ├── domain/
│ │ ├── constants/ # MessageTypes, SettingsDefaults, StorageKeys, StreamingPlatforms
│ │ ├── entities/ # SubtitleCue, SubtitleTrack, AiSubState, Context, WatchHistory
│ │ ├── value-objects/ # Language, CacheKey
│ │ └── repositories/ # Repository interfaces
│ ├── infrastructure/
│ │ ├── platform/ # BrowserPlatformFactory (Chrome vs Firefox API abstraction)
│ │ └── storage/ # BrowserStorageFactory (Local/Sync/IndexedDB storage abstraction)
│ └── utils/ # Logger, i18n, hash utilities
│
├── injected/ # MAIN World Script
│ ├── domain/ # IInjectedAdapter, InjectedAdapterRegistry, message payload types
│ └── infrastructure/
│ ├── network/ # NetworkHook (fetch/XHR hooking), SubtitleStreamDetector
│ └── adapters/ # Platform implementations: netflix/, tver/, youtube/
│
├── content/ # ISOLATED World Script
│ ├── domain/ # IStreamingAdapter, StreamingAdapterRegistry, IAdDetector
│ ├── application/ # SubtitleManager, ContentMessageService, SubtitleParser
│ ├── infrastructure/
│ │ └── adapters/ # Platform implementations: netflix/, tver/, youtube/
│ └── presentation/ # SubtitleOverlay, DOM positioning, CSS styles
│
├── background/ # Background Service Worker / Background Script
│ ├── domain/ # Translation & LLM contracts, pipeline interfaces
│ ├── application/services/ # TranslationPipelineService, GlossaryService, VocabService, LlmService
│ ├── infrastructure/
│ │ ├── cache/ # TrackCacheStore (IndexedDB), memory caches
│ │ ├── llm/ # OpenRouterAdapter, OllamaAdapter
│ │ └── translation/ # Batch translators, supervisor reviewers
│ └── presentation/ # Runtime message routing and event dispatching
│
├── popup/ # Extension popup UI
├── options/ # Settings & preferences page
└── editor/ # Subtitle & glossary editor4. Subtitle Lifecycle & Data Flow
sequenceDiagram
autonumber
actor User as User / Browser
participant Injected as Injected Script (MAIN)
participant Content as Content Script (ISOLATED)
participant BG as Background Service Worker
participant LLM as AI Provider (OpenRouter / Ollama)
participant DOM as Player Overlay DOM
User->>Injected: Starts video playback
Injected->>Injected: Intercepts subtitle network response (VTT / TTML / XML)
Injected->>Content: window.postMessage(NCW_SUBTITLE_RAW_DATA)
Content->>Content: ContentAdapter.parseSubtitle() -> SubtitleCueData[]
Content->>BG: chrome.runtime.sendMessage(NCW_GET_SUBTITLE_CACHE)
alt Cache Hit (Already translated)
BG-->>Content: Returns cached SubtitleCue[]
else Cache Miss (Needs translation)
Content->>BG: chrome.runtime.sendMessage(NCW_TRANSLATE_BATCH)
BG->>LLM: Streams translation request with glossary & context
LLM-->>BG: AI translation & supervisor reviewed cues
BG->>BG: Store in IndexedDB (TrackCacheStore)
BG-->>Content: Dispatches translated cues
end
loop Playback Synchronization (video.ontimeupdate / requestAnimationFrame)
Content->>Content: SubtitleManager queries video.currentTime
Content->>DOM: SubtitleOverlay updates active cue text (Original + Translated)
end5. How to Add Support for a New Streaming Platform
Follow this strict step-by-step checklist to introduce support for a new streaming platform (e.g. disney, hulu, crunchyroll):
Step 1: Register Platform Identifier
Add the platform identifier to [src/shared/domain/constants/StreamingPlatforms.ts](file:///home/tom/Documents/codes/javascript/cowatcher-extension-public/src/shared/domain/constants/StreamingPlatforms.ts):
export const STREAMING_PLATFORMS = {
NETFLIX: "netflix",
TVER: "tver",
YOUTUBE: "youtube",
DISNEY: "disney", // Added
} as const;Step 2: Implement MAIN World Injected Adapter
Create src/injected/infrastructure/adapters/<platform>/<Platform>InjectedAdapter.ts implementing IInjectedAdapter:
Check URL matching in
matches(url: string).Use
NetworkHookto sniff subtitle URLs and API responses, or hook platform player globals.Emit
NCW_SUBTITLE_RAW_DATA,NCW_METADATA_LOADED, andNCW_EPISODE_CHANGEDviawindow.postMessage.Register the adapter in [
src/injected/injected.ts](file:///home/tom/Documents/codes/javascript/cowatcher-extension-public/src/injected/injected.ts):typescriptInjectedAdapterRegistry.register(new DisneyInjectedAdapter());
Step 3: Implement ISOLATED World Content Adapter
Create src/content/infrastructure/adapters/<platform>/<Platform>ContentAdapter.ts implementing IStreamingAdapter:
getVideoElement(): Return the active<video>element (ornullif not found).getPlayerContainer(): Return the specific player container (e.g..video-player-container).IMPORTANT
NEVER return
document.body. If the container is not found, returnnull. Ensure the container hasposition: relativeorposition: absolute.parseSubtitle(rawText: string): Convert raw platform subtitle data into normalizedSubtitleCueData[].Register the adapter in [
src/content/content.ts](file:///home/tom/Documents/codes/javascript/cowatcher-extension-public/src/content/content.ts):typescriptStreamingAdapterRegistry.register(new DisneyContentAdapter());
Step 4: Update Manifest Permissions & Matches
Add the platform's URL patterns to both manifest.json (Chrome) and manifest.firefox.json (Firefox) under content_scripts.matches and host_permissions.
Step 5: Verification & Quality Gate
Run type checking and verify builds for both targets:
npx tsc --noEmit
npm run build:all6. Cross-Browser Build Architecture
CoWatcher compiles dual distribution builds from a single TypeScript source tree:
flowchart LR
Source["TypeScript Source Code\n(src/)"]
PackageJson["package.json\n(SSOT: version, name, description)"]
ChromeManifest["manifest.json\n(MV3 Service Worker)"]
FirefoxManifest["manifest.firefox.json\n(MV3 Background Scripts + Gecko ID)"]
Webpack["Webpack 5 + ts-loader + CopyPlugin"]
DistChrome["dist/\n(Chrome Unpacked Extension)"]
DistFirefox["dist-firefox/\n(Firefox Temporary Add-on)"]
Source --> Webpack
PackageJson -. Dynamic Injection .-> Webpack
ChromeManifest -->|npm run build:chrome| Webpack --> DistChrome
FirefoxManifest -->|npm run build:firefox| Webpack --> DistFirefox- Chrome Target (
dist/):- Uses
manifest.json. - Service worker background:
"background": { "service_worker": "background/background.js" }.
- Uses
- Firefox Target (
dist-firefox/):- Uses
manifest.firefox.json. - Event page background:
"background": { "scripts": ["background/background.js"] }. - Includes
browser_specific_settings.geckowith an explicit extension ID and minimum Firefox version.
- Uses
- Dynamic Manifest Sync:
webpack.config.cjsutilizescopy-webpack-pluginwith a customtransformhook to guarantee thatversion,displayName, anddescriptionfrompackage.jsonare automatically injected into both manifests upon build.