Skip to content

Commit ddb696a

Browse files
authored
Merge pull request #69 from nebulabroadcast/new-proxy-player
New proxy player
2 parents ac3081a + 9d9688f commit ddb696a

29 files changed

+1441
-611
lines changed

backend/api/proxy.py

Lines changed: 57 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -7,22 +7,44 @@
77
from server.dependencies import CurrentUser
88
from server.request import APIRequest
99

10+
MAX_200_SIZE = 1024 * 1024 * 12
11+
1012

1113
class ProxyResponse(Response):
1214
content_type = "video/mp4"
1315

1416

17+
def get_file_size(file_name: str) -> int:
18+
"""Get the size of a file"""
19+
if not os.path.exists(file_name):
20+
raise nebula.NotFoundException("File not found")
21+
return os.stat(file_name).st_size
22+
23+
1524
async def get_bytes_range(file_name: str, start: int, end: int) -> bytes:
1625
"""Get a range of bytes from a file"""
1726
async with aiofiles.open(file_name, mode="rb") as f:
1827
await f.seek(start)
1928
pos = start
20-
# read_size = min(CHUNK_SIZE, end - pos + 1)
2129
read_size = end - pos + 1
2230
return await f.read(read_size)
2331

2432

2533
def _get_range_header(range_header: str, file_size: int) -> tuple[int, int]:
34+
"""
35+
Parse the Range header to determine the start and end byte positions.
36+
37+
Args:
38+
range_header (str): The value of the Range header from the HTTP request.
39+
file_size (int): The total size of the file in bytes.
40+
41+
Returns:
42+
tuple[int, int]: A tuple containing the start and end byte positions.
43+
44+
Raises:
45+
HTTPException: If the range is invalid or cannot be parsed.
46+
"""
47+
2648
def _invalid_range() -> HTTPException:
2749
return HTTPException(
2850
status.HTTP_416_REQUESTED_RANGE_NOT_SATISFIABLE,
@@ -45,15 +67,24 @@ async def range_requests_response(
4567
request: Request, file_path: str, content_type: str
4668
) -> ProxyResponse:
4769
"""Returns StreamingResponse using Range Requests of a given file"""
48-
file_size = os.stat(file_path).st_size
49-
max_chunk_size = 1024 * 1024 # 2MB
70+
71+
file_size = get_file_size(file_path)
72+
73+
max_chunk_size = 1024 * 1024 * 4
5074
range_header = request.headers.get("range")
75+
max_200_size = MAX_200_SIZE
76+
77+
# screw firefox
78+
if ua := request.headers.get("user-agent"):
79+
if "firefox" in ua.lower():
80+
max_chunk_size = file_size
81+
elif "safari" in ua.lower():
82+
max_200_size = 0
5183

5284
headers = {
5385
"content-type": content_type,
54-
"accept-ranges": "bytes",
55-
"content-encoding": "identity",
5686
"content-length": str(file_size),
87+
"accept-ranges": "bytes",
5788
"access-control-expose-headers": (
5889
"content-type, accept-ranges, content-length, "
5990
"content-range, content-encoding"
@@ -63,16 +94,31 @@ async def range_requests_response(
6394
end = file_size - 1
6495
status_code = status.HTTP_200_OK
6596

66-
if range_header is not None:
97+
if file_size <= max_200_size:
98+
# if the file has a sane size, we return the whole thing
99+
# in one go. That allows the browser to cache the video
100+
# and prevent unnecessary requests.
101+
102+
headers["content-range"] = f"bytes 0-{end}/{file_size}"
103+
104+
elif range_header is not None:
67105
start, end = _get_range_header(range_header, file_size)
68-
end = min(end, start + max_chunk_size - 1)
106+
end = min(end, start + max_chunk_size - 1, file_size - 1)
107+
69108
size = end - start + 1
70109
headers["content-length"] = str(size)
71110
headers["content-range"] = f"bytes {start}-{end}/{file_size}"
72-
status_code = status.HTTP_206_PARTIAL_CONTENT
111+
112+
if size == file_size:
113+
status_code = status.HTTP_200_OK
114+
else:
115+
status_code = status.HTTP_206_PARTIAL_CONTENT
73116

74117
payload = await get_bytes_range(file_path, start, end)
75118

119+
if status_code == status.HTTP_200_OK:
120+
headers["cache-control"] = "private, max-age=600"
121+
76122
return ProxyResponse(
77123
content=payload,
78124
headers=headers,
@@ -87,9 +133,9 @@ class ServeProxy(APIRequest):
87133
the file in media players that support HTTPS pseudo-streaming.
88134
"""
89135

90-
name: str = "proxy"
91-
path: str = "/proxy/{id_asset}"
92-
title: str = "Serve proxy"
136+
name = "proxy"
137+
path = "/proxy/{id_asset}"
138+
title = "Serve proxy"
93139
methods = ["GET"]
94140

95141
async def handle(
@@ -98,7 +144,6 @@ async def handle(
98144
id_asset: int,
99145
user: CurrentUser,
100146
) -> ProxyResponse:
101-
assert user
102147
sys_settings = nebula.settings.system
103148
proxy_storage_path = nebula.storages[sys_settings.proxy_storage].local_path
104149
proxy_path_template = os.path.join(proxy_storage_path, sys_settings.proxy_path)

frontend/src/components/BaseInput.jsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,9 +39,9 @@ const BaseInput = styled.input`
3939
}
4040
4141
&.timecode {
42-
min-width: 96px;
43-
max-width: 96px;
44-
padding-right: 14px !important;
42+
min-width: 92px;
43+
max-width: 92px;
44+
padding-right: 10px !important;
4545
text-align: right;
4646
font-family: monospace;
4747
}

frontend/src/components/Canvas.jsx

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { useEffect, forwardRef } from 'react'
2+
import styled from 'styled-components'
3+
4+
const CanvasContainer = styled.div`
5+
position: relative;
6+
padding: 0;
7+
margin: 0;
8+
9+
canvas {
10+
position: absolute;
11+
top: 0;
12+
left: 0;
13+
bottom: 0;
14+
right: 0;
15+
}
16+
`
17+
18+
const Canvas = forwardRef(({ style, onDraw, ...props }, ref) => {
19+
useEffect(() => {
20+
if (!ref.current) return
21+
const canvas = ref.current
22+
23+
const handleResize = () => {
24+
canvas.width = canvas.parentElement.clientWidth
25+
canvas.height = canvas.parentElement.clientHeight
26+
if (onDraw) {
27+
onDraw({ target: canvas })
28+
}
29+
}
30+
31+
handleResize()
32+
33+
const parentElement = canvas.parentElement
34+
const resizeObserver = new ResizeObserver(handleResize)
35+
resizeObserver.observe(parentElement)
36+
37+
return () => resizeObserver.unobserve(parentElement)
38+
}, [ref])
39+
40+
return (
41+
<CanvasContainer style={style}>
42+
<canvas ref={ref} {...props} />
43+
</CanvasContainer>
44+
)
45+
})
46+
Canvas.displayName = 'Canvas'
47+
48+
export default Canvas

frontend/src/components/Dropdown.jsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ const Dropdown = ({
8181
if (align === 'right') contentStyle['right'] = 0
8282

8383
return (
84-
<DropdownContainer className={clsx({disabled})}>
84+
<DropdownContainer className={clsx({ disabled })}>
8585
<Button
8686
className="dropbtn"
8787
style={buttonStyle}

frontend/src/components/InputDatetime.jsx

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -68,13 +68,7 @@ const CalendarDialog = ({ value, onChange, onClose }) => {
6868
)
6969
}
7070

71-
const InputDatetime = ({
72-
value,
73-
onChange,
74-
placeholder,
75-
mode,
76-
className,
77-
}) => {
71+
const InputDatetime = ({ value, onChange, placeholder, mode, className }) => {
7872
const [time, setTime] = useState()
7973
const [isFocused, setIsFocused] = useState(false)
8074
const [showCalendar, setShowCalendar] = useState(false)
@@ -155,7 +149,7 @@ const InputDatetime = ({
155149
value={time || ''}
156150
onChange={handleChange}
157151
style={{ flexGrow: 1 }}
158-
className={clsx(className, {error: !isValidTime(time)})}
152+
className={clsx(className, { error: !isValidTime(time) })}
159153
placeholder={isFocused ? timestampFormat : placeholder}
160154
title={`Please enter a valid time in the format ${timestampFormat}`}
161155
onBlur={onSubmit}

frontend/src/components/InputTimecode.jsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ const InputTimecode = ({
1717

1818
useEffect(() => {
1919
setInvalid(false)
20-
if (value === null || value === undefined) {
20+
if (value === null || value === undefined || isNaN(value)) {
2121
setText('')
2222
return
2323
}
@@ -71,13 +71,14 @@ const InputTimecode = ({
7171
onSubmit()
7272
inputRef.current.blur()
7373
}
74+
e.stopPropagation()
7475
}
7576

7677
return (
7778
<BaseInput
7879
type="text"
7980
ref={inputRef}
80-
className={clsx('timecode', className, {error: invalid})}
81+
className={clsx('timecode', className, { error: invalid })}
8182
value={text}
8283
onChange={onChangeHandler}
8384
onKeyDown={onKeyDown}

0 commit comments

Comments
 (0)