-
Notifications
You must be signed in to change notification settings - Fork 23
custom video service
This guide walks through creating a new media service for Cinema (Fixed Edition) from scratch.
Services live under:
cinema_modded/gamemode/modules/theater/services/
Every service is a Lua table that inherits from the base service via theater.RegisterService.
URL pasted / requested
│
▼
theater.ExtractURLData(url) -- parses URL, finds matching service
│
▼
SERVICE:Match(url) -- does this service handle the host?
│
▼
SERVICE:GetURLInfo(url) -- extracts video ID + optional start time
│
▼
SERVICE:GetVideoInfo(data, ok, fail) -- fetches title, duration, thumbnail
│
▼
VIDEO object created & queued
│
▼
CLIENT: SERVICE:LoadProvider(Video, panel) -- opens embed / HTML player
│
▼
JavaScript sets window.cinema_controller
exTheater.controllerReady() -- volume, seek, pause become available
The base service (sh_base.lua) already implements HTTP helpers, the DHTML crawler, the CinemaPlayer JS bridge, and LoadVideo. You only override what you need.
Create a new file, e.g. sh_myservice.lua:
local SERVICE = {
Name = "My Service", -- shown in UI / history
IsTimed = true, -- true = seekable VOD; false = live / infinite
IsCacheable = true, -- store in cinema_history DB
NeedsCodecFix = false, -- set true if H.264 / proprietary codecs needed
ExtentedVideoInfo = false, -- true = GetVideoInfo receives full video table
TheaterType = THEATER_NONE, -- THEATER_NONE | THEATER_PRIVATE | …
NeedsExtraChecks = false, -- advanced request-button control
}
-- 1) Can this service handle the URL?
function SERVICE:Match(url)
return url.host and url.host:match("example%.com")
end
-- 2) Extract stable video ID (+ optional start time)
function SERVICE:GetURLInfo(url)
local info = {}
-- Example: https://example.com/watch/abc123?t=30
if url.path then
local id = url.path:match("^/watch/([%w%-_]+)")
if id then
info.Data = id
end
end
if url.query and url.query.t then
local t = tonumber(url.query.t)
if t and t > 0 then
info.StartTime = t
end
end
return info.Data and info or false
end
-- 3) Fetch metadata (title, duration, thumbnail)
function SERVICE:GetVideoInfo(data, onSuccess, onFailure)
-- data is the string returned as info.Data above
local apiUrl = ("https://api.example.com/v1/videos/%s"):format(data)
self:Fetch(apiUrl, function(body, length, headers, code)
local response = util.JSONToTable(body)
if not response then
return onFailure("Theater_RequestFailed")
end
local info = {
title = response.title or "Unknown",
duration = tonumber(response.duration) or 0, -- seconds; 0 = live
thumbnail = response.thumbnail_url or "",
}
-- Optional: switch to a live variant
-- if response.is_live then
-- info.type = "myservicelive"
-- info.duration = 0
-- end
pcall(onSuccess, info)
end, onFailure)
end
-- 4) Client: load the actual player
if CLIENT then
local EMBED_URL = "https://example.com/embed/%s?autoplay=1"
local THEATER_JS = [[
(function() {
var checkerInterval = setInterval(function() {
var player = document.querySelector("video");
if (player && player.readyState >= 3) {
clearInterval(checkerInterval);
window.cinema_controller = player;
exTheater.controllerReady();
}
}, 50);
})();
]]
function SERVICE:LoadProvider(Video, panel)
local startTime = 0
if self.IsTimed then
startTime = math.max(0, math.Round(CurTime() - Video:StartTime()))
end
local url = EMBED_URL:format(Video:Data())
if self.IsTimed and startTime > 0 then
url = url .. "&start=" .. startTime
end
panel:OpenURL(url)
panel.OnDocumentReady = function(pnl)
self:LoadExFunctions(pnl) -- injects CinemaPlayer bridge
pnl:RunJavascript(THEATER_JS)
end
end
end
theater.RegisterService("myservice", SERVICE)That is enough for a working VOD service.
| Property | Type | Default | Meaning |
|---|---|---|---|
Name |
string | "Base" |
Display name |
IsTimed |
bool | true |
Supports seeking / has finite duration |
IsCacheable |
bool | true |
Allowed in cinema_history
|
NeedsCodecFix |
bool | false |
Requires GModPatchTool |
ExtentedVideoInfo |
bool | false |
GetVideoInfo receives full video object |
TheaterType |
flag | THEATER_NONE |
Restrict to certain theater types |
NeedsExtraChecks |
bool | false |
Custom request-button logic via JS |
Hidden |
bool | false |
Not shown in service lists (used for live variants) |
-
urlis a parsed table (url.host,url.path,url.query, …) from the shared URL library. - Return any truthy value when this service should handle the link.
- Keep it cheap; it is called for every request against every registered service.
Must return either:
{
Data = "stable-video-id", -- required, string
StartTime = 42, -- optional, seconds
}or false if the URL cannot be parsed.
Data is what later appears as Video:Data() and is passed to GetVideoInfo / LoadProvider.
- Runs on the server for most services (or triggers a client crawler).
- Call
onSuccess(info)with:
{
title = "Video Title",
duration = 123, -- seconds; 0 = treated as live
thumbnail = "https://…", -- optional
type = "myservicelive" -- optional: force another service class
}- Call
onFailure("Theater_RequestFailed")(or any i18n key / string) on error.
Use the inherited helper:
self:Fetch(url, onReceive, onFailure [, extraHeaders])-
panelis a DHTML panel that will fill the theater screen. - Open an embed URL or inject HTML, then make sure JavaScript eventually does:
window.cinema_controller = <HTMLMediaElement or compatible object>;
exTheater.controllerReady();The base method LoadExFunctions injects a window.theater CinemaPlayer that supports setVolume, seek, play, pause, and sync.
Many platforms need a second, non-timed service for live content:
theater.RegisterService("myservice", SERVICE)
theater.RegisterService("myservicelive", {
Name = "My Service Live",
IsTimed = false,
NeedsCodecFix = true,
Hidden = true, -- players never pick this directly
LoadProvider = CLIENT and SERVICE.LoadProvider or function() end
})In GetVideoInfo, when you detect a live stream:
info.type = "myservicelive"
info.duration = 0The theater system then switches the video type automatically.
If the platform has no usable server API, implement GetMetadata on the client:
if CLIENT then
function SERVICE:GetMetadata(data, callback)
local panel = self:CreateWebCrawler(callback)
panel:OpenURL("https://example.com/watch/" .. data)
panel.OnDocumentReady = function(pnl)
pnl:QueueJavascript([[
// extract title / duration and print:
// console.log("METADATA:" + JSON.stringify({title, duration, isLive}));
// or console.log("ERROR:message");
]])
end
end
endCreateWebCrawler returns an invisible DHTML panel that listens for METADATA: / ERROR: console messages and then calls your callback.
The theater expects a controller object with roughly this interface (already provided by LoadExFunctions when you assign an HTML5 <video>):
window.cinema_controller = videoElement; // must support:
// .volume (0–1)
// .currentTime
// .play() / .pause()
// .readyStateFor custom players (iframe APIs, etc.) you can implement a thin adapter:
window.cinema_controller = {
get volume() { return api.getVolume() / 100; },
set volume(v) { api.setVolume(v * 100); },
get currentTime() { return api.getCurrentTime(); },
set currentTime(t) { api.seekTo(t); },
play() { api.play(); },
pause() { api.pause(); },
readyState: 4
};
exTheater.controllerReady();| File | Pattern to study |
|---|---|
sh_dailymotion.lua |
Clean API + live dual-service |
sh_youtube.lua |
Client crawler, pause offset, hash parameters |
sh_url.lua |
Direct HTML5 / image / audio fallback |
sh_bilibili*.lua |
Multi-part IDs, several related services |
sh_jellyfin.lua |
Self-hosted media server |
sh_soundcloud.lua |
Audio-only + extended video info |
Always start from sh_dailymotion.lua or the skeleton above unless you need crawler complexity.
sh_services.lua does:
include("services/sh_base.lua") -- must be first
Loader.Load("modules/theater/services") -- loads every sh_*.luaYour file is picked up automatically as long as it is named sh_something.lua inside the services folder and calls theater.RegisterService.
- Restart the server / changelevel after adding the file.
- In a theater, paste a matching URL → request should appear in the queue.
- Confirm title, duration and thumbnail look correct.
- Confirm playback starts for all players in the theater.
- Test seek / pause / volume if
IsTimed = true. - Test a live URL if you registered a live variant.
- Check console for JavaScript errors (
cinema_html_filter 1helps). - Verify
NeedsCodecFixbehaviour with and without GModPatchTool.
-
Forgetting
theater.RegisterService→ service never appears. -
Returning non-string
Data→ later code breaks. -
Not calling
exTheater.controllerReady()→ volume/seek stay broken. -
Blocking the main thread in
GetVideoInfo→ useself:Fetch(async). -
Hard-coding HTTP instead of
self:Fetch→ missing User-Agent / headers. -
Overlapping
Matchwith another service → first registered match wins; be specific. -
Live streams with
IsTimed = true→ seek UI appears and confuses players.
- Video Services – list of built-in providers
- Development – module layout
-
Commands –
cinema_video_requestetc.