Play Videos in VidHub from Third-Party Apps and Services
Third-party apps can use a URL scheme to ask VidHub to play one video. VidHub currently provides two integration methods:
- Legacy
/open: retained for existing Apple-platform integrations; supports a video URL and external subtitles. - New
/play: based on x-callback-url; supports a starting position and returns playback progress and status when playback exits.
The new /play method supports iPhone, iPad, Apple TV, Mac, Android Mobile, and Android TV.
New integrations should use
/play. Existing Apple-platform/openintegrations remain supported. Android Mobile and Android TV support only/play.
Platform Support
| Platform | Legacy /open |
New /play |
|---|---|---|
| iPhone / iPad | Supported | Supported |
| Apple TV | Supported | Supported |
| Mac | Supported | Supported |
| Android Mobile | Not supported | Supported |
| Android TV | Not supported | Supported |
Feature Comparison
| Feature | Legacy /open |
New /play |
|---|---|---|
| Play one video | Supported | Supported |
| Load external subtitles | Supported | Supported |
| Set starting position | Not supported | Supported |
| Set display file name | Not supported | Supported |
| Return final playback position | Not supported | Supported |
| Distinguish completion from early exit | Not supported | Supported |
| Standard success, error, and cancel callbacks | Incomplete | Supported |
| Android Mobile / TV | Not supported | Supported |
Legacy Method: /open
open-vidhub://x-callback-url/open
/open is retained for existing Apple-platform integrations and does not support Android.
Parameters
| Parameter | Required | Description |
|---|---|---|
url |
Yes | Video URL |
sub |
No | External subtitle URL |
on-success |
No | URL called when VidHub opens the playback page successfully |
on-failed |
No | URL called when VidHub cannot open playback |
Basic Example
open-vidhub://x-callback-url/open?url=http%3A%2F%2Flocalhost%3A8080%2Fsample.mp4&on-success=some-app%3A%2F%2Fx-callback-url%2Fsuccess&on-failed=some-app%3A%2F%2Fx-callback-url%2Ffailed
Example with Subtitles
open-vidhub://x-callback-url/open?url=http%3A%2F%2Flocalhost%3A8080%2Fsample.mp4&sub=http%3A%2F%2Flocalhost%3A8080%2Fsample.srt&on-success=some-app%3A%2F%2Fx-callback-url%2Fsuccess&on-failed=some-app%3A%2F%2Fx-callback-url%2Ffailed
/opendoes not return the current position when the user exits and cannot distinguish completed playback from an early exit.
New Method: /play
open-vidhub://x-callback-url/play
/play uses the same address, parameters, and callback fields on iPhone, iPad, Apple TV, Mac, Android Mobile, and Android TV. It is designed for using VidHub as an external player. A calling app can provide its saved position, then receive the latest progress when playback exits. One call supports one video URL.
Request Parameters
| Parameter | Required | Description |
|---|---|---|
url |
Yes | Video URL to play |
position |
No | Starting position in seconds; must be a valid number from 0 through 31536000 |
filename |
No | File name or title shown in the player |
sub |
No | External subtitle URL |
x-success |
No | Callback after playback completes or the user exits playback |
x-error |
No | Callback for invalid parameters, a busy player, or playback failure |
x-cancel |
No | Callback when the user cancels before playback formally begins |
x-source |
No | Calling app name; identifies the request source and does not affect playback |
request-id |
No | Caller-generated identifier returned unchanged in callbacks |
url, sub, and every callback must be a complete URL with a scheme. A callback may not use the open-vidhub scheme.
Apple Swift Example
Use URLComponents and URLQueryItem to avoid manual encoding errors:
func playWithVidHub(
mediaURL: URL,
subtitleURL: URL? = nil,
position: Double = 0,
filename: String? = nil,
requestID: String = UUID().uuidString
) {
var components = URLComponents()
components.scheme = "open-vidhub"
components.host = "x-callback-url"
components.path = "/play"
var queryItems = [
URLQueryItem(name: "url", value: mediaURL.absoluteString),
URLQueryItem(name: "position", value: String(max(0, position))),
URLQueryItem(name: "request-id", value: requestID),
URLQueryItem(name: "x-source", value: "My App"),
URLQueryItem(name: "x-success", value: "myapp://x-callback-url/success"),
URLQueryItem(name: "x-error", value: "myapp://x-callback-url/error"),
URLQueryItem(name: "x-cancel", value: "myapp://x-callback-url/cancel")
]
if let filename, !filename.isEmpty {
queryItems.append(URLQueryItem(name: "filename", value: filename))
}
if let subtitleURL {
queryItems.append(URLQueryItem(name: "sub", value: subtitleURL.absoluteString))
}
components.queryItems = queryItems
guard let vidHubURL = components.url else { return }
#if os(macOS)
NSWorkspace.shared.open(vidHubURL)
#else
UIApplication.shared.open(vidHubURL, options: [:], completionHandler: nil)
#endif
}
Android Mobile / TV Kotlin Example
Android Mobile and Android TV use the same ACTION_VIEW intent:
import android.app.Activity
import android.content.ActivityNotFoundException
import android.content.Context
import android.content.Intent
import android.net.Uri
import java.util.UUID
fun playWithVidHub(
context: Context,
mediaUrl: Uri,
subtitleUrl: Uri? = null,
position: Double = 0.0,
filename: String? = null,
requestId: String = UUID.randomUUID().toString(),
) {
val callbackBase = "myapp://x-callback-url"
val vidHubUri = Uri.Builder()
.scheme("open-vidhub")
.authority("x-callback-url")
.appendPath("play")
.appendQueryParameter("url", mediaUrl.toString())
.appendQueryParameter("position", position.coerceAtLeast(0.0).toString())
.appendQueryParameter("request-id", requestId)
.appendQueryParameter("x-source", "My App")
.appendQueryParameter("x-success", "$callbackBase/success")
.appendQueryParameter("x-error", "$callbackBase/error")
.appendQueryParameter("x-cancel", "$callbackBase/cancel")
.apply {
filename?.takeIf { it.isNotBlank() }?.let {
appendQueryParameter("filename", it)
}
subtitleUrl?.let { appendQueryParameter("sub", it.toString()) }
}
.build()
val intent = Intent(Intent.ACTION_VIEW, vidHubUri).apply {
if (context !is Activity) addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
}
try {
context.startActivity(intent)
} catch (_: ActivityNotFoundException) {
// VidHub is not installed, or this version does not support the request.
}
}
Example call:
open-vidhub://x-callback-url/play?url=https%3A%2F%2Fexample.com%2Fvideos%2Fmovie.m3u8&sub=https%3A%2F%2Fexample.com%2Fsubtitles%2Fmovie-en.srt&position=120&filename=Example%20Movie.mp4&request-id=movie-123&x-source=My%20App&x-success=myapp%3A%2F%2Fx-callback-url%2Fsuccess&x-error=myapp%3A%2F%2Fx-callback-url%2Ferror&x-cancel=myapp%3A%2F%2Fx-callback-url%2Fcancel
Use URLComponents, URLQueryItem, or Android Uri.Builder for percent encoding instead of assembling the complete URL manually.
Callback Rules
Each /play request triggers at most one terminal callback:
- Playback completes or the user exits the player:
x-success. - The request or playback fails:
x-error. - The user cancels before playback formally begins:
x-cancel.
If a callback URL for that outcome is omitted, VidHub ends the flow without calling a different callback type.
x-success
| Parameter | Description |
|---|---|
lastPlayedUrl |
URL that was actually played |
position |
Playback position on exit, in whole seconds rounded down |
duration |
Total duration in seconds; may be omitted if unavailable |
status |
finished or stopped |
request-id |
Original request identifier, when supplied |
Completed playback:
myapp://x-callback-url/success?lastPlayedUrl=https%3A%2F%2Fexample.com%2Fvideo.m3u8&position=599&duration=600&status=finished&request-id=movie-123
Early exit:
myapp://x-callback-url/success?lastPlayedUrl=https%3A%2F%2Fexample.com%2Fvideo.m3u8&position=38&duration=600&status=stopped&request-id=movie-123
finished: mark the content played and clear or retain the resume position according to your app's rules.stopped: savepositionand pass it back in the next/playrequest.
x-error
| Parameter | Description |
|---|---|
errorCode |
Error code |
errorMessage |
Error description |
failedUrl |
Failed video URL, when available |
request-id |
Original request identifier, when supplied |
myapp://x-callback-url/error?errorCode=200&errorMessage=Playback%20failed&failedUrl=https%3A%2F%2Fexample.com%2Fvideo.m3u8&request-id=movie-123
| Code | Meaning |
|---|---|
100 |
Missing url parameter |
101 |
Invalid video URL |
102 |
Invalid position |
103 |
Unsupported video URL scheme |
104 |
Invalid callback URL |
200 |
Playback failed |
201 |
Player is busy and cannot accept another external request |
202 |
Player cannot be presented |
x-cancel
x-cancel applies only before playback formally starts and does not include progress. It returns request-id only when one was supplied:
myapp://x-callback-url/cancel?request-id=movie-123
Exiting after entering the player is not a cancellation; it calls x-success with status=stopped.
Receiving Callbacks
The calling app must register its own URL scheme. These examples use myapp://; replace it with your app's actual scheme.
Apple Platforms
UIKit with scenes:
func scene(_ scene: UIScene, openURLContexts URLContexts: Set<UIOpenURLContext>) {
guard let url = URLContexts.first?.url else { return }
handleVidHubCallback(url)
}
SwiftUI:
.onOpenURL { url in
handleVidHubCallback(url)
}
Android Mobile / TV
Register an intent filter for the callback activity:
<activity android:name=".VidHubCallbackActivity" android:exported="true">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="myapp" android:host="x-callback-url" />
</intent-filter>
</activity>
Read the callback path and parameters from intent.data:
val callbackUri = intent?.data ?: return
when (callbackUri.path) {
"/success" -> {
val position = callbackUri.getQueryParameter("position")?.toDoubleOrNull()
val duration = callbackUri.getQueryParameter("duration")?.toDoubleOrNull()
val status = callbackUri.getQueryParameter("status")
val requestId = callbackUri.getQueryParameter("request-id")
}
"/error" -> {
val errorCode = callbackUri.getQueryParameter("errorCode")
val errorMessage = callbackUri.getQueryParameter("errorMessage")
}
"/cancel" -> {
val requestId = callbackUri.getQueryParameter("request-id")
}
}
Android TV usually has no web browser, so use a callback scheme registered by the calling app rather than relying on an HTTPS web callback.
URL Encoding
Pass raw values to the platform URL builder and let it percent-encode them. Do not encode a value manually before handing it to URLQueryItem or Uri.Builder, or it may be encoded twice. Legacy /open retains its existing encoding behavior for compatibility.
Migrating from /open to /play
- Change the path from
/opento/play. - Rename
on-successtox-success. - Rename
on-failedtox-error. - Add
x-cancelandrequest-idif needed. - Pass the locally saved playback position as
position. - Read
position,duration, andstatusfrom the callback. - Use the system URL builder for encoding.
Android Mobile and Android TV should use /play directly.
Notes
- Each request supports one video URL.
/playuses the same fields on iPhone, iPad, Apple TV, Mac, Android Mobile, and Android TV.- The VidHub device must be able to access the video and subtitle URLs.
- A media URL may use any network or local scheme supported by VidHub, but not
open-vidhub. - Callback URLs must use a scheme registered by the third-party app, not
open-vidhub. - If no app handles the callback scheme, VidHub does not retry indefinitely.
- A callback cannot be guaranteed if the app is force-quit, the system shuts down, or the process crashes.
- These callbacks synchronize playback progress for this request and do not depend on Trakt login or Trakt Scrobbling.