Skip to content

Commit d9a2ad2

Browse files
authored
Merge pull request #70 from TypeType-Video/dev
feat: prepare server 1.2.0
2 parents e885c20 + c96e38c commit d9a2ad2

100 files changed

Lines changed: 4462 additions & 304 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

build.gradle.kts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ dependencies {
3636
implementation("io.ktor:ktor-server-call-logging-jvm")
3737
implementation("io.ktor:ktor-server-rate-limit-jvm")
3838
implementation("ch.qos.logback:logback-classic:1.5.38")
39-
implementation("com.github.InfinityLoop1308.PipePipeExtractor:extractor:00db0e1d9b553d941f3009eb79e492d95eaf442d")
39+
implementation("com.github.Priveetee.PipePipeExtractor:extractor:d1a723d382014c2428b273c92bc9fc8c15d59c83")
4040
compileOnly("com.github.TeamNewPipe:nanojson:1d9e1aea9049fc9f85e68b43ba39fe7be1c1f751")
4141
implementation("org.json:json:20260522")
4242
implementation("com.squareup.okhttp3:okhttp:5.4.0")

docs/android-playback.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# Android playback contract
2+
3+
TypeType-Android discovers this contract through `androidPlayback` in
4+
`GET /api/instance`. Contract version 1 supports completed YouTube VODs only;
5+
active livestreams remain explicitly unsupported.
6+
7+
Android playback sessions use `/api/android/youtube/playback/*` and are isolated
8+
from the web player's `/api/sabr/playback/*` sessions, generations, caches, and
9+
window protocol. A ready VOD manifest is a complete static DASH presentation
10+
from time zero. The server requires exact audio and video segment indexes, but
11+
continues to fetch media bytes on demand.
12+
13+
Creating a session may return `202` while the exact indexes are preparing. A
14+
seek keeps the session ID and selected itags stable, increments the generation,
15+
and makes older media URLs return `409`. Unknown sessions return `404`; recently
16+
expired sessions return `410`. Session manifests and media remain same-origin
17+
and use `Cache-Control: no-store`.

openapi.yaml

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ tags:
1414
- name: extraction
1515
- name: downloader
1616
- name: youtube-session
17+
- name: android-playback
1718
- name: user-data
1819
paths:
1920
/health: { $ref: ./openapi/paths/health.yaml#/Health }
@@ -27,6 +28,12 @@ paths:
2728
/streams/bilibili: { $ref: ./openapi/paths/streams.yaml#/BiliBiliStreams }
2829
/streams/audio-only: { $ref: ./openapi/paths/streams.yaml#/AudioOnly }
2930
/streams/audio-only/source: { $ref: ./openapi/paths/streams.yaml#/AudioOnlySource }
31+
/android/youtube/playback/{videoId}: { $ref: ./openapi/paths/android-playback.yaml#/Create }
32+
/android/youtube/playback/{sessionId}/seek: { $ref: ./openapi/paths/android-playback.yaml#/Seek }
33+
/android/youtube/playback/{sessionId}/manifest.mpd: { $ref: ./openapi/paths/android-playback.yaml#/Manifest }
34+
/android/youtube/playback/{sessionId}/subtitles/{trackId}.vtt: { $ref: ./openapi/paths/android-playback.yaml#/Subtitle }
35+
/android/youtube/playback/{sessionId}/{itag}/init: { $ref: ./openapi/paths/android-playback.yaml#/Initialization }
36+
/android/youtube/playback/{sessionId}/{itag}/segment/{sequence}: { $ref: ./openapi/paths/android-playback.yaml#/Segment }
3037
/search: { $ref: ./openapi/paths/search.yaml#/Search }
3138
/search/filters: { $ref: ./openapi/paths/search.yaml#/SearchFilters }
3239
/channel: { $ref: ./openapi/paths/channel.yaml#/Channel }
@@ -37,6 +44,7 @@ paths:
3744
/playlist: { $ref: ./openapi/paths/playlists.yaml#/Playlist }
3845
/saved-playlists: { $ref: ./openapi/paths/saved-playlists.yaml#/SavedPlaylists }
3946
/saved-playlists/{id}: { $ref: ./openapi/paths/saved-playlists.yaml#/SavedPlaylist }
47+
/subscriptions/feed: { $ref: ./openapi/paths/subscriptions.yaml#/SubscriptionFeed }
4048
/settings: { $ref: ./openapi/paths/access-control.yaml#/Settings }
4149
/allowed/channels: { $ref: ./openapi/paths/access-control.yaml#/AllowedChannels }
4250
/allowed/channels/{channelUrl}: { $ref: ./openapi/paths/access-control.yaml#/AllowedChannel }
@@ -88,6 +96,12 @@ components:
8896
$ref: ./openapi/components/instance.yaml#/InstanceMinClientVersion
8997
InstanceResponse:
9098
$ref: ./openapi/components/instance.yaml#/InstanceResponse
99+
AndroidPlaybackCreateRequest:
100+
$ref: ./openapi/components/android-playback.yaml#/AndroidPlaybackCreateRequest
101+
AndroidPlaybackSeekRequest:
102+
$ref: ./openapi/components/android-playback.yaml#/AndroidPlaybackSeekRequest
103+
AndroidPlaybackResponse:
104+
$ref: ./openapi/components/android-playback.yaml#/AndroidPlaybackResponse
91105
StreamResponse:
92106
$ref: ./openapi/components/streams.yaml#/StreamResponse
93107
AudioOnlyStreamResponse:
@@ -118,6 +132,8 @@ components:
118132
$ref: ./openapi/components/media.yaml#/PublicPlaylistItem
119133
SavedPlaylistItem: { $ref: ./openapi/components/media.yaml#/SavedPlaylistItem }
120134
SavedPlaylistRequest: { $ref: ./openapi/components/media.yaml#/SavedPlaylistRequest }
135+
SubscriptionFeedResponse: { $ref: ./openapi/components/subscriptions.yaml#/SubscriptionFeedResponse }
136+
SubscriptionFeedPreparingResponse: { $ref: ./openapi/components/subscriptions.yaml#/SubscriptionFeedPreparingResponse }
121137
SettingsItem: { $ref: ./openapi/components/access-control.yaml#/SettingsItem }
122138
AdminSettingsItem: { $ref: ./openapi/components/access-control.yaml#/AdminSettingsItem }
123139
AllowedChannelItem: { $ref: ./openapi/components/access-control.yaml#/AllowedChannelItem }
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
AndroidPlaybackCreateRequest:
2+
type: object
3+
properties:
4+
videoItag: { type: integer, nullable: true }
5+
audioItag: { type: integer, nullable: true }
6+
audioTrackId: { type: string, nullable: true }
7+
AndroidPlaybackSeekRequest:
8+
type: object
9+
required: [generation, playerTimeMs]
10+
properties:
11+
generation: { type: integer, format: int64, minimum: 0 }
12+
playerTimeMs: { type: integer, format: int64, minimum: 0 }
13+
AndroidPlaybackResponse:
14+
type: object
15+
required:
16+
- sessionId
17+
- videoId
18+
- manifestUrl
19+
- videoItag
20+
- audioItag
21+
- generation
22+
- ready
23+
- status
24+
- subtitles
25+
properties:
26+
sessionId: { type: string }
27+
videoId: { type: string }
28+
manifestUrl: { type: string }
29+
videoItag: { type: integer }
30+
audioItag: { type: integer }
31+
audioTrackId: { type: string, nullable: true }
32+
generation: { type: integer, format: int64, minimum: 0 }
33+
ready: { type: boolean }
34+
status: { type: string, enum: [ready, preparing] }
35+
subtitles:
36+
type: array
37+
items: { $ref: '#/AndroidSubtitle' }
38+
retryAfterMs: { type: integer, format: int64, nullable: true, minimum: 100, maximum: 2000 }
39+
AndroidSubtitle:
40+
type: object
41+
required: [id, mimeType, languageTag, displayLanguageName, isAutoGenerated, url]
42+
properties:
43+
id: { type: string }
44+
mimeType: { type: string, enum: [text/vtt] }
45+
languageTag: { type: string }
46+
displayLanguageName: { type: string }
47+
isAutoGenerated: { type: boolean }
48+
url:
49+
type: string
50+
description: Session-scoped same-origin resource that remains stable across playback generations.

openapi/components/instance.yaml

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ InstanceResponse:
2020
- oidcAutoRedirect
2121
- youtubeRemoteLoginEnabled
2222
- youtubeRemoteLoginReady
23+
- androidPlayback
2324
properties:
2425
name: { type: string, example: TypeType }
2526
tagline: { type: string, nullable: true }
@@ -49,3 +50,20 @@ InstanceResponse:
4950
type: string
5051
nullable: true
5152
enum: [disabled, not_configured, token_unreachable]
53+
androidPlayback:
54+
$ref: '#/AndroidPlaybackCapability'
55+
AndroidPlaybackCapability:
56+
type: object
57+
required: [supported, contractVersion, youtube]
58+
properties:
59+
supported: { type: boolean, example: true }
60+
contractVersion: { type: integer, enum: [2] }
61+
youtube:
62+
$ref: '#/AndroidYoutubePlaybackCapability'
63+
AndroidYoutubePlaybackCapability:
64+
type: object
65+
required: [vod, live, subtitles]
66+
properties:
67+
vod: { type: boolean, example: true }
68+
live: { type: boolean, example: false }
69+
subtitles: { type: boolean, example: true }
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
SubscriptionFeedResponse:
2+
type: object
3+
required: [videos, nextpage, generation, generatedAt, refreshing]
4+
properties:
5+
videos:
6+
type: array
7+
maxItems: 100
8+
items:
9+
$ref: ./media.yaml#/VideoItem
10+
nextpage:
11+
type: string
12+
nullable: true
13+
description: Opaque continuation bound to this snapshot generation and page size.
14+
generation:
15+
type: integer
16+
format: int64
17+
generatedAt:
18+
type: integer
19+
format: int64
20+
description: Unix timestamp in milliseconds for the snapshot build.
21+
refreshing:
22+
type: boolean
23+
description: True while a stale snapshot is being rebuilt in the background.
24+
SubscriptionFeedPreparingResponse:
25+
type: object
26+
required: [code, retryAfterMs]
27+
properties:
28+
code:
29+
type: string
30+
enum: [subscription_feed_preparing]
31+
retryAfterMs:
32+
type: integer
33+
format: int64
34+
minimum: 100
35+
maximum: 5000
Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
Create:
2+
parameters:
3+
- name: videoId
4+
in: path
5+
required: true
6+
schema: { type: string }
7+
post:
8+
tags: [android-playback]
9+
summary: Create an Android-only YouTube VOD playback session
10+
description: Active livestreams are rejected. Media remains demand-driven after the complete virtual index is ready. The response includes the authoritative subtitle inventory before Media3 prepares the item.
11+
requestBody:
12+
required: false
13+
content:
14+
application/json:
15+
schema: { $ref: ../components/android-playback.yaml#/AndroidPlaybackCreateRequest }
16+
responses:
17+
'200': { $ref: '#/components/responses/ReadySession' }
18+
'202': { $ref: '#/components/responses/PreparingSession' }
19+
'400': { $ref: ../components/common.yaml#/JsonError }
20+
'401': { $ref: ../components/common.yaml#/JsonError }
21+
'403': { $ref: ../components/common.yaml#/JsonError }
22+
'422': { $ref: ../components/common.yaml#/JsonError }
23+
'503': { $ref: ../components/common.yaml#/JsonError }
24+
Seek:
25+
parameters:
26+
- $ref: '#/components/parameters/SessionId'
27+
post:
28+
tags: [android-playback]
29+
summary: Reposition an Android VOD playback session
30+
description: The session ID and selected formats stay stable while the generation increments.
31+
requestBody:
32+
required: true
33+
content:
34+
application/json:
35+
schema: { $ref: ../components/android-playback.yaml#/AndroidPlaybackSeekRequest }
36+
responses:
37+
'200': { $ref: '#/components/responses/ReadySession' }
38+
'202': { $ref: '#/components/responses/PreparingSession' }
39+
'400': { $ref: ../components/common.yaml#/JsonError }
40+
'404': { $ref: ../components/common.yaml#/JsonError }
41+
'409': { $ref: ../components/common.yaml#/JsonError }
42+
'410': { $ref: ../components/common.yaml#/JsonError }
43+
'422': { $ref: ../components/common.yaml#/JsonError }
44+
Manifest:
45+
parameters:
46+
- $ref: '#/components/parameters/SessionId'
47+
get:
48+
tags: [android-playback]
49+
summary: Get the complete static DASH presentation for an Android VOD session
50+
responses:
51+
'200':
52+
description: Complete standards-compatible static DASH MPD.
53+
headers:
54+
Cache-Control:
55+
schema: { type: string, example: no-store }
56+
content:
57+
application/dash+xml:
58+
schema: { type: string }
59+
'202': { $ref: '#/components/responses/PreparingSession' }
60+
'404': { $ref: ../components/common.yaml#/JsonError }
61+
'410': { $ref: ../components/common.yaml#/JsonError }
62+
'422': { $ref: ../components/common.yaml#/JsonError }
63+
Subtitle:
64+
parameters:
65+
- $ref: '#/components/parameters/SessionId'
66+
- $ref: '#/components/parameters/TrackId'
67+
get:
68+
tags: [android-playback]
69+
summary: Get one session-scoped Android subtitle as UTF-8 WebVTT
70+
description: The caller must use the same account context that created the playback session. The resource remains valid across seek generations.
71+
responses:
72+
'200':
73+
description: Complete UTF-8 WebVTT subtitle document.
74+
headers:
75+
Cache-Control:
76+
schema: { type: string, example: no-store }
77+
content:
78+
text/vtt:
79+
schema: { type: string }
80+
'401': { $ref: ../components/common.yaml#/JsonError }
81+
'404': { $ref: ../components/common.yaml#/JsonError }
82+
'410': { $ref: ../components/common.yaml#/JsonError }
83+
'422': { $ref: ../components/common.yaml#/JsonError }
84+
'503': { $ref: ../components/common.yaml#/JsonError }
85+
Initialization:
86+
parameters:
87+
- $ref: '#/components/parameters/SessionId'
88+
- $ref: '#/components/parameters/Itag'
89+
- $ref: '#/components/parameters/SessionQuery'
90+
- $ref: '#/components/parameters/Generation'
91+
get:
92+
tags: [android-playback]
93+
summary: Get initialization bytes for a selected Android playback track
94+
responses:
95+
'200': { description: Initialization bytes. }
96+
'202': { $ref: '#/components/responses/PreparingSession' }
97+
'206': { description: Partial initialization bytes. }
98+
'400': { $ref: ../components/common.yaml#/JsonError }
99+
'404': { $ref: ../components/common.yaml#/JsonError }
100+
'409': { $ref: ../components/common.yaml#/JsonError }
101+
'410': { $ref: ../components/common.yaml#/JsonError }
102+
Segment:
103+
parameters:
104+
- $ref: '#/components/parameters/SessionId'
105+
- $ref: '#/components/parameters/Itag'
106+
- name: sequence
107+
in: path
108+
required: true
109+
schema: { type: integer, minimum: 1 }
110+
- $ref: '#/components/parameters/SessionQuery'
111+
- $ref: '#/components/parameters/Generation'
112+
get:
113+
tags: [android-playback]
114+
summary: Get one demand-driven media segment for an Android playback track
115+
responses:
116+
'200': { description: Media segment bytes. }
117+
'202': { $ref: '#/components/responses/PreparingSession' }
118+
'206': { description: Partial media segment bytes. }
119+
'400': { $ref: ../components/common.yaml#/JsonError }
120+
'404': { $ref: ../components/common.yaml#/JsonError }
121+
'409': { $ref: ../components/common.yaml#/JsonError }
122+
'410': { $ref: ../components/common.yaml#/JsonError }
123+
components:
124+
parameters:
125+
SessionId:
126+
name: sessionId
127+
in: path
128+
required: true
129+
schema: { type: string }
130+
Itag:
131+
name: itag
132+
in: path
133+
required: true
134+
schema: { type: integer }
135+
TrackId:
136+
name: trackId
137+
in: path
138+
required: true
139+
schema: { type: string }
140+
SessionQuery:
141+
name: session
142+
in: query
143+
required: true
144+
schema: { type: string }
145+
Generation:
146+
name: generation
147+
in: query
148+
required: true
149+
schema: { type: integer, format: int64, minimum: 0 }
150+
responses:
151+
ReadySession:
152+
description: Playback session with a complete VOD presentation.
153+
content:
154+
application/json:
155+
schema: { $ref: ../components/android-playback.yaml#/AndroidPlaybackResponse }
156+
PreparingSession:
157+
description: The exact virtual segment index or requested bytes are still preparing.
158+
content:
159+
application/json:
160+
schema: { $ref: ../components/android-playback.yaml#/AndroidPlaybackResponse }

0 commit comments

Comments
 (0)