Skip to content

Commit 075ab51

Browse files
committed
merge squashed feature/mp4-streaming zedeus#634
Signed-off-by: r3g_5z <[email protected]>
1 parent 6871745 commit 075ab51

File tree

5 files changed

+100
-70
lines changed

5 files changed

+100
-70
lines changed

src/prefs_impl.nim

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ genPrefs:
8080

8181
Media:
8282
mp4Playback(checkbox, true):
83-
"Enable mp4 video playback (only for gifs)"
83+
"Enable mp4 video playback"
8484

8585
hlsPlayback(checkbox, false):
8686
"Enable hls video streaming (requires JavaScript)"

src/routes/media.nim

Lines changed: 66 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ export httpclient, os, strutils, asyncstreams, base64, re
1212

1313
const
1414
m3u8Mime* = "application/vnd.apple.mpegurl"
15+
mp4Mime* = "video/mp4"
1516
maxAge* = "max-age=604800"
1617

1718
proc safeFetch*(url: string): Future[string] {.async.} =
@@ -20,56 +21,81 @@ proc safeFetch*(url: string): Future[string] {.async.} =
2021
except: discard
2122
finally: client.close()
2223

23-
template respond*(req: asynchttpserver.Request; headers) =
24-
var msg = "HTTP/1.1 200 OK\c\L"
25-
for k, v in headers:
24+
template respond*(req: asynchttpserver.Request; code: HttpCode;
25+
headers: seq[(string, string)]) =
26+
var msg = "HTTP/1.1 " & $code & "\c\L"
27+
for (k, v) in headers:
2628
msg.add(k & ": " & v & "\c\L")
2729

2830
msg.add "\c\L"
29-
yield req.client.send(msg)
31+
yield req.client.send(msg, flags={})
32+
33+
proc getContentLength(res: AsyncResponse): string =
34+
result = "0"
35+
if res.headers.hasKey("content-length"):
36+
result = $res.contentLength
37+
elif res.headers.hasKey("content-range"):
38+
result = res.headers["content-range"]
39+
result = result[result.find('/') + 1 .. ^1]
40+
if result == "*":
41+
result.setLen(0)
3042

3143
proc proxyMedia*(req: jester.Request; url: string): Future[HttpCode] {.async.} =
3244
result = Http200
45+
3346
let
3447
request = req.getNativeReq()
35-
client = newAsyncHttpClient()
48+
hashed = $hash(url)
49+
50+
if request.headers.getOrDefault("If-None-Match") == hashed:
51+
return Http304
52+
53+
let c = newAsyncHttpClient(headers=newHttpHeaders({
54+
"accept": "*/*",
55+
"range": $req.headers.getOrDefault("range")
56+
}))
3657

3758
try:
38-
let res = await client.get(url)
39-
if res.status != "200 OK":
59+
var res = await c.get(url)
60+
if not res.status.startsWith("20"):
4061
return Http404
4162

42-
let hashed = $hash(url)
43-
if request.headers.getOrDefault("If-None-Match") == hashed:
44-
return Http304
63+
var headers = @{
64+
"Accept-Ranges": "bytes",
65+
"Content-Type": res.headers["content-type", 0],
66+
"Cache-Control": maxAge
67+
}
4568

46-
let contentLength =
47-
if res.headers.hasKey("content-length"):
48-
res.headers["content-length", 0]
49-
else:
50-
""
69+
var tries = 0
70+
while tries <= 10 and res.headers.hasKey("transfer-encoding"):
71+
await sleepAsync(100 + tries * 200)
72+
res = await c.get(url)
73+
tries.inc
5174

52-
let headers = newHttpHeaders({
53-
"Content-Type": res.headers["content-type", 0],
54-
"Content-Length": contentLength,
55-
"Cache-Control": maxAge,
56-
"ETag": hashed
57-
})
75+
let contentLength = res.getContentLength
76+
if contentLength.len > 0:
77+
headers.add ("Content-Length", contentLength)
5878

59-
respond(request, headers)
79+
if res.headers.hasKey("content-range"):
80+
headers.add ("Content-Range", $res.headers.getOrDefault("content-range"))
81+
respond(request, Http206, headers)
82+
else:
83+
respond(request, Http200, headers)
6084

6185
var (hasValue, data) = (true, "")
6286
while hasValue:
6387
(hasValue, data) = await res.bodyStream.read()
6488
if hasValue:
65-
await request.client.send(data)
89+
await request.client.send(data, flags={})
6690
data.setLen 0
67-
except HttpRequestError, ProtocolError, OSError:
91+
except OSError: discard
92+
except ProtocolError, HttpRequestError:
6893
result = Http404
6994
finally:
70-
client.close()
95+
c.close()
7196

72-
template check*(code): untyped =
97+
template check*(c): untyped =
98+
let code = c
7399
if code != Http200:
74100
resp code
75101
else:
@@ -83,37 +109,27 @@ proc decoded*(req: jester.Request; index: int): string =
83109
if based: decode(encoded)
84110
else: decodeUrl(encoded)
85111

112+
proc getPicUrl*(req: jester.Request): string =
113+
result = decoded(req, 1)
114+
if "twimg.com" notin result:
115+
result.insert(twimg)
116+
if not result.startsWith(https):
117+
result.insert(https)
118+
86119
proc createMediaRouter*(cfg: Config) =
87120
router media:
88121
get "/pic/?":
89122
resp Http404
90123

91124
get re"^\/pic\/orig\/(enc)?\/?(.+)":
92-
var url = decoded(request, 1)
93-
if "twimg.com" notin url:
94-
url.insert(twimg)
95-
if not url.startsWith(https):
96-
url.insert(https)
97-
url.add("?name=orig")
98-
99-
let uri = parseUri(url)
100-
cond isTwitterUrl(uri) == true
101-
102-
let code = await proxyMedia(request, url)
103-
check code
125+
let url = getPicUrl(request)
126+
cond isTwitterUrl(parseUri(url)) == true
127+
check await proxyMedia(request, url & "?name=orig")
104128

105129
get re"^\/pic\/(enc)?\/?(.+)":
106-
var url = decoded(request, 1)
107-
if "twimg.com" notin url:
108-
url.insert(twimg)
109-
if not url.startsWith(https):
110-
url.insert(https)
111-
112-
let uri = parseUri(url)
113-
cond isTwitterUrl(uri) == true
114-
115-
let code = await proxyMedia(request, url)
116-
check code
130+
let url = getPicUrl(request)
131+
cond isTwitterUrl(parseUri(url)) == true
132+
check await proxyMedia(request, url)
117133

118134
get re"^\/video\/(enc)?\/?(.+)\/(.+)$":
119135
let url = decoded(request, 2)
@@ -123,8 +139,7 @@ proc createMediaRouter*(cfg: Config) =
123139
resp showError("Failed to verify signature", cfg)
124140

125141
if ".mp4" in url or ".ts" in url or ".m4s" in url:
126-
let code = await proxyMedia(request, url)
127-
check code
142+
check await proxyMedia(request, url)
128143

129144
var content: string
130145
if ".vmap" in url:

src/utils.nim

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# SPDX-License-Identifier: AGPL-3.0-only
22
import strutils, strformat, uri, tables, base64
33
import nimcrypto
4+
import types
45

56
var
67
hmacKey: string
@@ -28,6 +29,20 @@ proc setProxyEncoding*(state: bool) =
2829
proc getHmac*(data: string): string =
2930
($hmac(sha256, hmacKey, data))[0 .. 12]
3031

32+
proc getBestMp4VidVariant(video: Video): VideoVariant =
33+
for v in video.variants:
34+
if v.bitrate >= result.bitrate:
35+
result = v
36+
37+
proc getVidVariant*(video: Video; playbackType: VideoType): VideoVariant =
38+
case playbackType
39+
of mp4:
40+
return video.getBestMp4VidVariant
41+
of m3u8, vmap:
42+
for variant in video.variants:
43+
if variant.contentType == playbackType:
44+
return variant
45+
3146
proc getVidUrl*(link: string): string =
3247
if link.len == 0: return
3348
let sig = getHmac(link)

src/views/renderutils.nim

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ proc genDate*(pref, state: string): VNode =
9090

9191
proc genImg*(url: string; class=""): VNode =
9292
buildHtml():
93-
img(src=getPicUrl(url), class=class, alt="")
93+
img(src=getPicUrl(url), class=class, alt="", loading="lazy", decoding="async")
9494

9595
proc getTabClass*(query: Query; tab: QueryKind): string =
9696
result = "tab-item"

src/views/tweet.nim

Lines changed: 17 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# SPDX-License-Identifier: AGPL-3.0-only
2-
import strutils, sequtils, strformat, options, algorithm
2+
import strutils, sequtils, strformat, options
33
import karax/[karaxdsl, vdom, vstyles]
44
from jester import Request
55

@@ -12,7 +12,7 @@ const doctype = "<!DOCTYPE html>\n"
1212
proc renderMiniAvatar(user: User; prefs: Prefs): VNode =
1313
let url = getPicUrl(user.getUserPic("_mini"))
1414
buildHtml():
15-
img(class=(prefs.getAvatarClass & " mini"), src=url)
15+
img(class=(prefs.getAvatarClass & " mini"), src=url, loading="lazy")
1616

1717
proc renderHeader(tweet: Tweet; retweet: string; prefs: Prefs): VNode =
1818
buildHtml(tdiv):
@@ -85,38 +85,38 @@ proc renderVideo*(video: Video; prefs: Prefs; path: string): VNode =
8585
let
8686
container = if video.description.len == 0 and video.title.len == 0: ""
8787
else: " card-container"
88-
playbackType = if not prefs.proxyVideos and video.hasMp4Url: mp4
88+
playbackType = if prefs.proxyVideos and video.hasMp4Url: mp4
8989
else: video.playbackType
9090

9191
buildHtml(tdiv(class="attachments card")):
9292
tdiv(class="gallery-video" & container):
9393
tdiv(class="attachment video-container"):
9494
let thumb = getSmallPic(video.thumb)
95-
if not video.available:
96-
img(src=thumb)
97-
renderVideoUnavailable(video)
98-
elif not prefs.isPlaybackEnabled(playbackType):
99-
img(src=thumb)
100-
renderVideoDisabled(playbackType, path)
101-
else:
95+
let canPlay = prefs.isPlaybackEnabled(playbackType)
96+
97+
if video.available and canPlay:
10298
let
103-
vars = video.variants.filterIt(it.contentType == playbackType)
104-
vidUrl = vars.sortedByIt(it.resolution)[^1].url
99+
vidUrl = video.getVidVariant(playbackType).url
105100
source = if prefs.proxyVideos: getVidUrl(vidUrl)
106101
else: vidUrl
107102
case playbackType
108103
of mp4:
109104
if prefs.muteVideos:
110-
video(poster=thumb, controls="", muted=""):
111-
source(src=source, `type`="video/mp4")
105+
video(src=source, poster=thumb, controls="", muted="", preload="metadata"):
112106
else:
113-
video(poster=thumb, controls=""):
114-
source(src=source, `type`="video/mp4")
107+
video(src=source, poster=thumb, controls="", preload="metadata"):
115108
of m3u8, vmap:
116109
video(poster=thumb, data-url=source, data-autoload="false")
117110
verbatim "<div class=\"video-overlay\" onclick=\"playVideo(this)\">"
118111
tdiv(class="overlay-circle"): span(class="overlay-triangle")
119112
verbatim "</div>"
113+
else:
114+
img(src=thumb, loading="lazy", decoding="async")
115+
if not canPlay:
116+
renderVideoDisabled(playbackType, path)
117+
else:
118+
renderVideoUnavailable(video)
119+
120120
if container.len > 0:
121121
tdiv(class="card-content"):
122122
h2(class="card-title"): text video.title
@@ -154,7 +154,7 @@ proc renderPoll(poll: Poll): VNode =
154154
proc renderCardImage(card: Card): VNode =
155155
buildHtml(tdiv(class="card-image-container")):
156156
tdiv(class="card-image"):
157-
img(src=getPicUrl(card.image), alt="")
157+
img(src=getPicUrl(card.image), alt="", loading="lazy")
158158
if card.kind == player:
159159
tdiv(class="card-overlay"):
160160
tdiv(class="overlay-circle"):

0 commit comments

Comments
 (0)