第三方应用和服务如何调用 VidHub API播放影片

第三方应用可以通过 URL Scheme 调用 VidHub 播放单个视频。VidHub 目前提供两种调用方式:

  • 旧版 /open:保留给已有的 Apple 平台接入,支持打开视频和外部字幕。
  • 新版 /play:基于 x-callback-url,支持指定起播位置,并在退出播放时返回播放进度和状态。

新版 /play 支持以下平台:

  • iPhone 和 iPad
  • Apple TV
  • Mac
  • Android Mobile
  • Android TV

新接入请优先使用 /play。已有 Apple 平台的 /open 调用仍可继续使用;Android Mobile 和 Android TV 目前只支持新版 /play

平台支持

平台 旧版 /open 新版 /play
iPhone / iPad 支持 支持
Apple TV 支持 支持
Mac 支持 支持
Android Mobile 不支持 支持
Android TV 不支持 支持

两种调用方式的区别

功能 旧版 /open 新版 /play
播放单个视频 支持 支持
加载外部字幕 支持 支持
指定起播位置 不支持 支持
指定显示文件名 不支持 支持
返回最终播放位置 不支持 支持
区分播放完成和中途退出 不支持 支持
标准成功、失败、取消回调 不完整 支持
Android Mobile / TV 不支持 支持

旧版方式:/open

旧版调用地址:

open-vidhub://x-callback-url/open

旧版 /open 用于兼容已有的 Apple 平台接入,不适用于 Android。

参数

参数 是否必填 说明
url 视频地址
sub 外部字幕地址
on-success VidHub 成功打开播放页时调用的 URL
on-failed VidHub 无法打开时调用的 URL

基础示例

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

带字幕的示例

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 不会在用户退出播放时返回当前播放位置,也无法通过回调区分“播放完成”和“中途退出”。

新版方式:/play

新版调用地址:

open-vidhub://x-callback-url/play

/play 在 iPhone、iPad、Apple TV、Mac、Android Mobile 和 Android TV 上使用相同的地址、参数和回调字段。

它适合将 VidHub 作为外部播放器使用。第三方应用可以传入上次的播放位置,VidHub 退出播放时再通过回调返回最新进度。

一次调用只支持一个视频地址。

请求参数

参数 是否必填 说明
url 要播放的视频地址
position 起播位置,单位为秒;必须是 0 到 31536000 之间的有效数字
filename 播放器中显示的文件名或标题
sub 外部字幕地址
x-success 播放结束或用户退出播放后的回调 URL
x-error 请求参数无效、播放器忙碌或播放失败时的回调 URL
x-cancel 正式开始播放前,用户取消操作时的回调 URL
x-source 调用方名称,用于标识请求来源;不影响播放
request-id 由调用方生成的请求标识,VidHub 会在回调中原样返回

urlsub 和回调 URL 都必须是包含 Scheme 的完整 URL。回调 URL 不能使用 open-vidhub Scheme,否则会被视为无效回调。

Apple 平台 Swift 调用示例

使用 URLComponentsURLQueryItem 生成调用地址,可以避免手动拼接参数时出现编码错误:

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 调用示例

Android Mobile 和 Android TV 使用相同的 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 未安装,或当前版本尚不支持该调用方式。
    }
}

调用示例:

open-vidhub://x-callback-url/play?url=https%3A%2F%2Fexample.com%2Fvideos%2Fmovie.m3u8&sub=https%3A%2F%2Fexample.com%2Fsubtitles%2Fmovie-zh.srt&position=120&filename=%E7%A4%BA%E4%BE%8B%E5%BD%B1%E7%89%87.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

实际使用时应由 URLComponentsURLQueryItem 或 Android Uri.Builder 完成百分号编码,不建议手工拼接完整 URL。

回调规则

每次 /play 请求最多触发一次终态回调:

  • 正常播放完成或进入播放器后退出:调用 x-success
  • 请求或播放失败:调用 x-error
  • 正式开始播放前取消:调用 x-cancel

如果调用方没有提供某类回调 URL,VidHub 会直接结束对应流程,不会尝试调用其他类型的回调。

x-success

VidHub 会在视频自然播放完成,或用户在播放过程中退出播放器时调用 x-success

参数 说明
lastPlayedUrl 实际播放的视频地址
position 退出时的播放位置,单位为秒,向下取整
duration 视频总时长,单位为秒;无法取得时可能不返回
status finishedstopped
request-id 请求时传入的 request-id;未传入时不返回

视频自然播放完成:

myapp://x-callback-url/success?lastPlayedUrl=https%3A%2F%2Fexample.com%2Fvideo.m3u8&position=599&duration=600&status=finished&request-id=movie-123

用户中途退出:

myapp://x-callback-url/success?lastPlayedUrl=https%3A%2F%2Fexample.com%2Fvideo.m3u8&position=38&duration=600&status=stopped&request-id=movie-123

建议调用方根据 status 处理进度:

  • finished:将内容标记为已播放,并按自身规则清除或保留续播位置。
  • stopped:保存 position,下次调用 /play 时将它传回 VidHub。

x-error

请求参数无效、播放器正忙、无法打开播放器或播放失败时,VidHub 会调用 x-error

参数 说明
errorCode 错误码
errorMessage 错误说明
failedUrl 失败的视频地址;有可用地址时返回
request-id 请求时传入的 request-id;未传入时不返回

示例:

myapp://x-callback-url/error?errorCode=200&errorMessage=Playback%20failed&failedUrl=https%3A%2F%2Fexample.com%2Fvideo.m3u8&request-id=movie-123

错误码:

错误码 说明
100 缺少 url 参数
101 视频 URL 无效
102 position 无效
103 不支持的视频 URL Scheme
104 回调 URL 无效
200 播放失败
201 播放器正忙,当前无法接受新的外部播放请求
202 无法显示播放器

x-cancel

x-cancel 只用于正式开始播放前的取消操作。回调不会附带播放进度,只会在请求中包含 request-id 时将其返回:

myapp://x-callback-url/cancel?request-id=movie-123

进入播放器后再退出不属于取消,此时会调用 x-success,并返回 status=stopped

接收回调

调用方需要注册自己的 URL Scheme。以下示例使用:

myapp://

请将 myapp 替换为你的应用实际注册的 Scheme,并确保 x-successx-errorx-cancel 使用该 Scheme。

Apple 平台

使用 Scene 的 UIKit 应用可以在 SceneDelegate 中接收回调:

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

Android 调用方需要为自己的回调 Activity 注册 Intent Filter:

<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>

然后从 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 通常没有网页浏览器,因此 TV 端建议使用调用方应用自己注册的回调 Scheme,而不是依赖 HTTPS 网页回调。

URL 编码注意事项

新版 /play 应把原始参数传给系统 URL 构造工具,由系统统一完成百分号编码:

URLQueryItem(name: "url", value: mediaURL.absoluteString)
uriBuilder.appendQueryParameter("url", mediaUrl.toString())

不要先手动编码,再交给 URLQueryItemUri.Builder,否则可能发生二次编码。

旧版 /open 继续保留原有编码方式,以兼容已有 Apple 平台接入。

/open 迁移到 /play

已有 Apple 平台接入可以按以下步骤迁移:

  1. 将路径从 /open 改为 /play
  2. on-success 改为 x-success
  3. on-failed 改为 x-error
  4. 根据需要增加 x-cancelrequest-id
  5. 将本地保存的播放进度通过 position 传给 VidHub。
  6. 在回调中读取 positiondurationstatus
  7. 新接口使用系统 URL 构造工具统一编码。

Android Mobile 和 Android TV 应直接使用 /play,无需兼容 /open

使用注意事项

  • 每次调用只支持一个视频 URL。
  • /play 在 iPhone、iPad、Apple TV、Mac、Android Mobile 和 Android TV 上使用相同的协议字段。
  • 视频和字幕地址必须能从运行 VidHub 的设备访问。
  • 视频地址可以使用 VidHub 支持的网络或本地 URL Scheme,但不能使用 open-vidhub 作为媒体地址的 Scheme。
  • 回调 URL 必须属于第三方应用自身注册的 Scheme,不能使用 open-vidhub
  • 如果第三方应用未安装,或没有应用能处理回调 Scheme,VidHub 不会持续重试。
  • 应用被强制结束、系统关机或进程异常退出时,无法保证一定发送最终回调。
  • 这套回调用于第三方应用与 VidHub 之间同步本次播放进度,不依赖 Trakt 登录或 Trakt Scrobbling。