Skip to content

Commit 7cf7fe6

Browse files
authored
Merge pull request #95 from anusii/tony/94_video
Tony/94_video
2 parents d7e83c8 + 2e79ef1 commit 7cf7fe6

2 files changed

Lines changed: 173 additions & 1 deletion

File tree

Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
### Static web server with HTTP Range request support
2+
###
3+
### Copyright (C) 2026, Software Innovation Institute, ANU.
4+
###
5+
### Licensed under the MIT License (the "License").
6+
###
7+
### License: https://choosealicense.com/licenses/mit/.
8+
##
9+
## Permission is hereby granted, free of charge, to any person obtaining a copy
10+
## of this software and associated documentation files (the "Software"), to deal
11+
## in the Software without restriction, including without limitation the rights
12+
## to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
13+
## copies of the Software, and to permit persons to whom the Software is
14+
## furnished to do so, subject to the following conditions:
15+
##
16+
## The above copyright notice and this permission notice shall be included in
17+
## all copies or substantial portions of the Software.
18+
##
19+
## THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
20+
## IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
21+
## FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
22+
## AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
23+
## LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
24+
## OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
25+
## SOFTWARE.
26+
###
27+
### Authors: Tony Chen
28+
29+
### Usage:
30+
### cd example
31+
### flutter build web --release
32+
### cd build/web
33+
### python ../../server/http_range_server.py 8080
34+
35+
import os
36+
import sys
37+
import mimetypes
38+
from http.server import HTTPServer, SimpleHTTPRequestHandler
39+
40+
41+
class RangeRequestHandler(SimpleHTTPRequestHandler):
42+
"""HTTP request handler with Range request support for media streaming."""
43+
44+
def send_head(self):
45+
"""Send response headers with Range request support."""
46+
path = self.translate_path(self.path)
47+
48+
if os.path.isdir(path):
49+
# Try to serve index.html for directory requests
50+
index_path = os.path.join(path, 'index.html')
51+
if os.path.exists(index_path):
52+
path = index_path
53+
else:
54+
# Fall back to directory listing
55+
return super().send_head()
56+
57+
if not os.path.exists(path):
58+
self.send_error(404, "File not found")
59+
return None
60+
61+
# Get file size and content type
62+
file_size = os.path.getsize(path)
63+
content_type, _ = mimetypes.guess_type(path)
64+
if content_type is None:
65+
content_type = 'application/octet-stream'
66+
67+
# Check for Range header
68+
range_header = self.headers.get('Range')
69+
70+
if range_header:
71+
# Parse Range header (e.g., "bytes=0-1023")
72+
try:
73+
range_spec = range_header.replace('bytes=', '')
74+
start_str, end_str = range_spec.split('-')
75+
start = int(start_str) if start_str else 0
76+
end = int(end_str) if end_str else file_size - 1
77+
78+
# Clamp values
79+
start = max(0, start)
80+
end = min(end, file_size - 1)
81+
82+
if start > end or start >= file_size:
83+
self.send_error(416, "Requested Range Not Satisfiable")
84+
self.send_header('Content-Range', f'bytes */{file_size}')
85+
self.end_headers()
86+
return None
87+
88+
content_length = end - start + 1
89+
90+
# Send 206 Partial Content
91+
self.send_response(206)
92+
self.send_header('Content-Type', content_type)
93+
self.send_header('Content-Length', str(content_length))
94+
self.send_header('Content-Range',
95+
f'bytes {start}-{end}/{file_size}')
96+
self.send_header('Accept-Ranges', 'bytes')
97+
self.send_header('Access-Control-Allow-Origin', '*')
98+
self.end_headers()
99+
100+
# Return file object positioned at start
101+
f = open(path, 'rb')
102+
f.seek(start)
103+
return _RangeFile(f, content_length)
104+
105+
except (ValueError, AttributeError):
106+
# Invalid Range header, fall through to normal response
107+
pass
108+
109+
# No Range header or invalid, send full file
110+
self.send_response(200)
111+
self.send_header('Content-Type', content_type)
112+
self.send_header('Content-Length', str(file_size))
113+
self.send_header('Accept-Ranges', 'bytes')
114+
self.send_header('Access-Control-Allow-Origin', '*')
115+
self.end_headers()
116+
117+
return open(path, 'rb')
118+
119+
def do_OPTIONS(self):
120+
"""Handle CORS preflight requests."""
121+
self.send_response(200)
122+
self.send_header('Access-Control-Allow-Origin', '*')
123+
self.send_header('Access-Control-Allow-Methods', 'GET, OPTIONS')
124+
self.send_header('Access-Control-Allow-Headers', 'Range')
125+
self.end_headers()
126+
127+
128+
class _RangeFile:
129+
"""Wrapper for file object to read only a specific range."""
130+
131+
def __init__(self, file_obj, length):
132+
self.file = file_obj
133+
self.remaining = length
134+
135+
def read(self, size=-1):
136+
if self.remaining <= 0:
137+
return b''
138+
if size < 0 or size > self.remaining:
139+
size = self.remaining
140+
data = self.file.read(size)
141+
self.remaining -= len(data)
142+
return data
143+
144+
def close(self):
145+
self.file.close()
146+
147+
148+
def run_server(port=8000):
149+
"""Start the HTTP server with Range request support."""
150+
server_address = ('', port)
151+
httpd = HTTPServer(server_address, RangeRequestHandler)
152+
print(f"Serving HTTP on http://localhost:{port}")
153+
print("Press Ctrl+C to stop the server")
154+
print("")
155+
print("This server supports HTTP Range requests for video seeking.")
156+
try:
157+
httpd.serve_forever()
158+
except KeyboardInterrupt:
159+
print("\nServer stopped.")
160+
httpd.server_close()
161+
162+
163+
if __name__ == '__main__':
164+
port = 8000
165+
if len(sys.argv) > 1:
166+
try:
167+
port = int(sys.argv[1])
168+
except ValueError:
169+
print(f"Invalid port: {sys.argv[1]}")
170+
sys.exit(1)
171+
172+
run_server(port)

lib/src/widgets/video_widget.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ class _VideoWidgetState extends State<VideoWidget> {
8383
// On web, load directly from assets as a URL.
8484
// media_kit on web uses HTML5 video which can load asset URLs.
8585

86-
mediaUri = rawLocalPath;
86+
mediaUri = 'assets/$rawLocalPath';
8787
} else {
8888
// On non-web platforms, check if file exists locally.
8989

0 commit comments

Comments
 (0)