
Open/close Widevine sessions, build/verify SignedMessage license requests/responses from PSSH, parse/convert PSSH boxes, extract and decrypt content keys, with protobuf models and multiplatform-safe crypto.
Kotlin Multiplatform library that mirrors the core functionality of pywidevine: open/close Widevine sessions, build signed license requests from PSSH, verify and parse license responses, and expose decrypted content keys.
Built on the same protobuf models as pywidevine and designed to run on JVM, Android, iOS and Linux targets.
API reference: https://samfun75.github.io/ktvine/ — conceptual guide in docs/API.md.
Gradle (Kotlin DSL):
dependencies {
implementation("io.github.samfun75:ktvine:1.0.0-RC1")
// Optional: talk to a pywidevine-compatible CDM server instead of holding a device.
// implementation("io.github.samfun75:ktvine-remote:1.0.0-RC1")
// Optional (JVM only): serve your own CDM over that same protocol.
// implementation("io.github.samfun75:ktvine-serve:1.0.0-RC1")
}This is a Kotlin Multiplatform library, published for JVM, Android, iOS (x64, arm64, simulator arm64) and linuxX64.
Two things to know before you start:
Cdm API is suspend. Call it from a coroutine. A Cdm is safe to share
between coroutines.kotlin.uuid.Uuid appears in the public API and is still experimental in Kotlin 2.2,
so you must opt in — @OptIn(ExperimentalUuidApi::class), or the
-opt-in=kotlin.uuid.ExperimentalUuidApi compiler flag.The per-symbol API reference is generated with Dokka and published at https://samfun75.github.io/ktvine/; docs/API.md is the conceptual guide.
The typical flow is the same as pywidevine, adapted to Kotlin:
import org.samfun.ktvine.core.Device
import org.samfun.ktvine.cdm.Cdm
val device = Device.loads(base64Wvd) // or Device.loads(bytes)
val cdm = Cdm.fromDevice(device)val sessionId = cdm.open() // suspend, like the rest of Cdm
// Optional: raw SignedDrmCertificate bytes, SignedMessage-wrapped bytes, or Base64.
// Cdm.COMMON_PRIVACY_CERT is bundled; otherwise POST Cdm.SERVICE_CERTIFICATE_CHALLENGE
// to your license server to obtain one.
// cdm.setServiceCertificate(sessionId, Cdm.COMMON_PRIVACY_CERT)import org.samfun.ktvine.core.PSSH
val pssh = PSSH(psshBase64) // or PSSH(psshBytes)
val challenge = cdm.getLicenseChallenge(
sessionId = sessionId,
pssh = pssh
)
// Send `challenge` bytes to your Widevine license server (not provided by this library)// licenseMessage: SignedMessage(LICENSE) payload from your server (raw bytes)
cdm.parseLicense(sessionId, licenseMessage)
val keys = cdm.getKeys(sessionId) // List<Key>; filter by KeyType with getKeys(sessionId, type)
keys.forEach { println(it) }
cdm.close(sessionId)PSSH parsing and conversion helpers are included:
PSSH(psshBase64), PSSH(psshBytes). Besides a full
pssh box these also accept a bare Widevine CENC header, a bare PlayReady header or
PlayReady Object, and — unless you pass strict = true — any custom init data, wrapped
verbatim in a v0 Widevine box.pssh.keyIds() → List<Uuid> (kotlin.uuid.Uuid)pssh.export() (bytes), pssh.exportBase64() (Base64)PSSH.parseAll(bytes), PSSH.fromInitSegment(bytes, systemId)
pssh.encryptionScheme (AESCTR, AESCBC, …), carried through conversionpssh.toWidevine()pssh.toPlayready(laUrl, luiUrl, dsId, decryptorSetup, customData) (builds v4.3.0.0 header)PSSH.new(systemId, keyIds = ..., initData = ..., version = 0/1)
pssh.setKeyIds(listOf(uuid1, uuid2)) (Widevine and PlayReady)Public methods throw typed exceptions you can catch:
All of them derive from KtvineException, so a single catch is enough:
suspend fun main() {
val device = Device.loads(System.getenv("WVD_BASE64"))
val cdm = Cdm.fromDevice(device)
val session = cdm.open()
val pssh = PSSH(System.getenv("PSSH_BASE64"))
val challenge = cdm.getLicenseChallenge(session, pssh)
val licenseMessage: ByteArray = postToYourServer(challenge) // implement yourself
cdm.parseLicense(session, licenseMessage)
cdm.getKeys(session).forEach { println(it) }
cdm.close(session)
}The device's private key never enters your process: every operation is an HTTP call to a
pywidevine-compatible CDM server, so one device can
back many clients. It implements the same CdmApi as Cdm, and you supply the Ktor engine,
so this module picks none for you.
dependencies {
implementation("io.github.samfun75:ktvine-remote:1.0.0-RC1")
implementation("io.ktor:ktor-client-cio:3.0.3") // any Ktor engine you like
}val cdm: CdmApi = RemoteCdm(
client = HttpClient(CIO),
baseUrl = "https://cdm.example.com",
deviceName = "my_device",
secret = System.getenv("KTVINE_SECRET"),
// Optional: open() then refuses a server holding a different device.
expectedSystemId = 4464,
expectedSecurityLevel = 3,
)
val session = cdm.open()
try {
val challenge = cdm.getLicenseChallenge(session, PSSH(psshBase64))
cdm.parseLicense(session, postToYourLicenseServer(challenge))
cdm.getKeys(session).forEach { println("${it.kid}: ${it.key.toHexString()}") }
} finally {
cdm.close(session)
}RequestType.RENEWAL and RELEASE are rejected locally — the serve protocol has no endpoint
for them.
The mirror image: hold the device once and serve it over that same protocol, so both ktvine's
RemoteCdm and pywidevine's own client can drive it. This module ships routing only — you
mount it in your own Ktor application and choose the engine. JVM only, because Ktor's server
engines do not span the targets the client does.
dependencies {
implementation("io.github.samfun75:ktvine-serve:1.0.0-RC1")
implementation("io.ktor:ktor-server-cio:3.0.3") // you pick the engine
}val config = ServeConfig(
devices = mapOf("my_device" to Device.loads(wvdBytes)),
users = mapOf(
System.getenv("KTVINE_SECRET") to ServeUser("alice", devices = setOf("my_device")),
),
// Refuse a challenge whose session has no service certificate.
forcePrivacyMode = true,
)
embeddedServer(CIO, port = 8786) {
routing { ktvineCdm(config) }
}.start(wait = true)Callers authenticate with an X-Secret-Key header. The device's private key never leaves the
server, so treat those secrets as credentials and serve this over TLS.
Every public declaration carries KDoc. The generated reference for all three modules lives at https://samfun75.github.io/ktvine/, and docs/API.md is the conceptual guide that explains what a signature cannot. Build the reference locally with:
./gradlew dokkaHtmlMultiModule # -> build/dokka/htmlMultiModule/index.html
See LICENSE.
Built with Claude Code.
Kotlin Multiplatform library that mirrors the core functionality of pywidevine: open/close Widevine sessions, build signed license requests from PSSH, verify and parse license responses, and expose decrypted content keys.
Built on the same protobuf models as pywidevine and designed to run on JVM, Android, iOS and Linux targets.
API reference: https://samfun75.github.io/ktvine/ — conceptual guide in docs/API.md.
Gradle (Kotlin DSL):
dependencies {
implementation("io.github.samfun75:ktvine:1.0.0-RC1")
// Optional: talk to a pywidevine-compatible CDM server instead of holding a device.
// implementation("io.github.samfun75:ktvine-remote:1.0.0-RC1")
// Optional (JVM only): serve your own CDM over that same protocol.
// implementation("io.github.samfun75:ktvine-serve:1.0.0-RC1")
}This is a Kotlin Multiplatform library, published for JVM, Android, iOS (x64, arm64, simulator arm64) and linuxX64.
Two things to know before you start:
Cdm API is suspend. Call it from a coroutine. A Cdm is safe to share
between coroutines.kotlin.uuid.Uuid appears in the public API and is still experimental in Kotlin 2.2,
so you must opt in — @OptIn(ExperimentalUuidApi::class), or the
-opt-in=kotlin.uuid.ExperimentalUuidApi compiler flag.The per-symbol API reference is generated with Dokka and published at https://samfun75.github.io/ktvine/; docs/API.md is the conceptual guide.
The typical flow is the same as pywidevine, adapted to Kotlin:
import org.samfun.ktvine.core.Device
import org.samfun.ktvine.cdm.Cdm
val device = Device.loads(base64Wvd) // or Device.loads(bytes)
val cdm = Cdm.fromDevice(device)val sessionId = cdm.open() // suspend, like the rest of Cdm
// Optional: raw SignedDrmCertificate bytes, SignedMessage-wrapped bytes, or Base64.
// Cdm.COMMON_PRIVACY_CERT is bundled; otherwise POST Cdm.SERVICE_CERTIFICATE_CHALLENGE
// to your license server to obtain one.
// cdm.setServiceCertificate(sessionId, Cdm.COMMON_PRIVACY_CERT)import org.samfun.ktvine.core.PSSH
val pssh = PSSH(psshBase64) // or PSSH(psshBytes)
val challenge = cdm.getLicenseChallenge(
sessionId = sessionId,
pssh = pssh
)
// Send `challenge` bytes to your Widevine license server (not provided by this library)// licenseMessage: SignedMessage(LICENSE) payload from your server (raw bytes)
cdm.parseLicense(sessionId, licenseMessage)
val keys = cdm.getKeys(sessionId) // List<Key>; filter by KeyType with getKeys(sessionId, type)
keys.forEach { println(it) }
cdm.close(sessionId)PSSH parsing and conversion helpers are included:
PSSH(psshBase64), PSSH(psshBytes). Besides a full
pssh box these also accept a bare Widevine CENC header, a bare PlayReady header or
PlayReady Object, and — unless you pass strict = true — any custom init data, wrapped
verbatim in a v0 Widevine box.pssh.keyIds() → List<Uuid> (kotlin.uuid.Uuid)pssh.export() (bytes), pssh.exportBase64() (Base64)PSSH.parseAll(bytes), PSSH.fromInitSegment(bytes, systemId)
pssh.encryptionScheme (AESCTR, AESCBC, …), carried through conversionpssh.toWidevine()pssh.toPlayready(laUrl, luiUrl, dsId, decryptorSetup, customData) (builds v4.3.0.0 header)PSSH.new(systemId, keyIds = ..., initData = ..., version = 0/1)
pssh.setKeyIds(listOf(uuid1, uuid2)) (Widevine and PlayReady)Public methods throw typed exceptions you can catch:
All of them derive from KtvineException, so a single catch is enough:
suspend fun main() {
val device = Device.loads(System.getenv("WVD_BASE64"))
val cdm = Cdm.fromDevice(device)
val session = cdm.open()
val pssh = PSSH(System.getenv("PSSH_BASE64"))
val challenge = cdm.getLicenseChallenge(session, pssh)
val licenseMessage: ByteArray = postToYourServer(challenge) // implement yourself
cdm.parseLicense(session, licenseMessage)
cdm.getKeys(session).forEach { println(it) }
cdm.close(session)
}The device's private key never enters your process: every operation is an HTTP call to a
pywidevine-compatible CDM server, so one device can
back many clients. It implements the same CdmApi as Cdm, and you supply the Ktor engine,
so this module picks none for you.
dependencies {
implementation("io.github.samfun75:ktvine-remote:1.0.0-RC1")
implementation("io.ktor:ktor-client-cio:3.0.3") // any Ktor engine you like
}val cdm: CdmApi = RemoteCdm(
client = HttpClient(CIO),
baseUrl = "https://cdm.example.com",
deviceName = "my_device",
secret = System.getenv("KTVINE_SECRET"),
// Optional: open() then refuses a server holding a different device.
expectedSystemId = 4464,
expectedSecurityLevel = 3,
)
val session = cdm.open()
try {
val challenge = cdm.getLicenseChallenge(session, PSSH(psshBase64))
cdm.parseLicense(session, postToYourLicenseServer(challenge))
cdm.getKeys(session).forEach { println("${it.kid}: ${it.key.toHexString()}") }
} finally {
cdm.close(session)
}RequestType.RENEWAL and RELEASE are rejected locally — the serve protocol has no endpoint
for them.
The mirror image: hold the device once and serve it over that same protocol, so both ktvine's
RemoteCdm and pywidevine's own client can drive it. This module ships routing only — you
mount it in your own Ktor application and choose the engine. JVM only, because Ktor's server
engines do not span the targets the client does.
dependencies {
implementation("io.github.samfun75:ktvine-serve:1.0.0-RC1")
implementation("io.ktor:ktor-server-cio:3.0.3") // you pick the engine
}val config = ServeConfig(
devices = mapOf("my_device" to Device.loads(wvdBytes)),
users = mapOf(
System.getenv("KTVINE_SECRET") to ServeUser("alice", devices = setOf("my_device")),
),
// Refuse a challenge whose session has no service certificate.
forcePrivacyMode = true,
)
embeddedServer(CIO, port = 8786) {
routing { ktvineCdm(config) }
}.start(wait = true)Callers authenticate with an X-Secret-Key header. The device's private key never leaves the
server, so treat those secrets as credentials and serve this over TLS.
Every public declaration carries KDoc. The generated reference for all three modules lives at https://samfun75.github.io/ktvine/, and docs/API.md is the conceptual guide that explains what a signature cannot. Build the reference locally with:
./gradlew dokkaHtmlMultiModule # -> build/dokka/htmlMultiModule/index.html
See LICENSE.
Built with Claude Code.