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 /open integrations 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

/open does 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: save position and pass it back in the next /play request.

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

  1. Change the path from /open to /play.
  2. Rename on-success to x-success.
  3. Rename on-failed to x-error.
  4. Add x-cancel and request-id if needed.
  5. Pass the locally saved playback position as position.
  6. Read position, duration, and status from the callback.
  7. Use the system URL builder for encoding.

Android Mobile and Android TV should use /play directly.

Notes

  • Each request supports one video URL.
  • /play uses 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.