Skip to content

Commit 48612fb

Browse files
📝
1 parent 3376272 commit 48612fb

File tree

1 file changed

+238
-95
lines changed

1 file changed

+238
-95
lines changed

blog/2024-5-31.md

Lines changed: 238 additions & 95 deletions
Original file line numberDiff line numberDiff line change
@@ -13,24 +13,126 @@ description: 记录使用flask搭建个人共享屏幕工具
1313

1414
这个时候,你就需要一个工具来捕获和分享你的屏幕和音频(包括设备音频和麦克风输入),并通过网页形式与他人共享。这样,观众无需下载任何会议软件,仅需打开浏览器即可观看。
1515

16+
当然有时候你需要提供必要的文件,比如代码,文档等。所以这个程序还允许你上传与下载文件。上传的文件会保存在当前目录下的`upload`文件夹中,你也可以从`upload`文件夹中下载文件。
17+
1618
## 安装依赖
1719

1820
```bash
19-
pip install opencv-python Pillow greenlet pyaudio Flask
21+
pip install flask opencv-python Pillow greenlet pyaudio Flask
2022
```
2123

24+
## 项目目录结构
25+
26+
项目下有两个文件夹: templates 和 uploads。
27+
28+
templates 文件夹中包含一个 upload.html 文件,用于页面展示。
29+
30+
uploads 文件夹用于保存上传的文件。
31+
32+
```bash showLineNumbers
33+
your_project/
34+
├── templates/
35+
│ └── upload.html
36+
│ └── stream.html
37+
├── uploads/
38+
└── app.py
39+
```
40+
41+
## 路由说明
42+
43+
`/` 路由。它处理文件上传、下载功能。
44+
45+
`/d` 路由。它提供桌面视频流+音频流。
46+
47+
`/c` 路由。它提供摄像头视频流+音频流。
48+
49+
2250
## 代码
2351

24-
```python showLineNumbers title="share.py"
25-
from flask import Flask, Response, render_template_string, stream_with_context
52+
```html showLineNumbers title="upload.html"
53+
<!doctype html>
54+
<html lang="zh">
55+
<head>
56+
<meta charset="UTF-8">
57+
<title>上传文件</title>
58+
</head>
59+
<body>
60+
<h1>上传文件</h1>
61+
<form method="post" enctype="multipart/form-data">
62+
<input type="file" name="file">
63+
<input type="submit" value="上传">
64+
</form>
65+
{% if filename %}
66+
<p>文件上传成功!</p>
67+
<a href="{{ url_for('download_file', filename=filename) }}">下载 {{ filename }}</a>
68+
{% endif %}
69+
</body>
70+
</html>
71+
```
72+
73+
```html showLineNumbers title="stream.html"
74+
<!DOCTYPE html>
75+
<head>
76+
<title>Intranet Broadcast</title>
77+
</head>
78+
79+
<body>
80+
<img id="video" src="{{ video_url }}" alt="视频流">
81+
<audio autoplay style="display:none;">
82+
<source src="{{ audio_url }}" type="audio/x-wav; codec=pcm">Your browser does not support the audio
83+
element.
84+
</audio>
85+
</body>
86+
</html>
87+
```
88+
89+
```python showLineNumbers title="app.py"
90+
from flask import Flask, render_template, request, send_from_directory, abort, Response,stream_with_context
91+
import os
2692
import time
2793
import cv2
28-
from PIL import ImageGrab, Image
94+
import numpy as np
95+
from PIL import ImageGrab
2996
import threading
30-
from io import BytesIO
3197
from greenlet import getcurrent as get_ident
3298
import pyaudio
99+
import logging
100+
101+
logging.basicConfig(level=logging.INFO)
102+
103+
app = Flask(__name__)
104+
105+
# 设置上传文件夹
106+
UPLOAD_FOLDER = "uploads"
107+
app.config["UPLOAD_FOLDER"] = UPLOAD_FOLDER
33108

109+
# 添加下载文件的路由
110+
@app.route("/download/<filename>")
111+
def download_file(filename):
112+
try:
113+
return send_from_directory(
114+
app.config["UPLOAD_FOLDER"], filename, as_attachment=True
115+
)
116+
except FileNotFoundError:
117+
abort(404)
118+
119+
@app.route("/", methods=["GET", "POST"])
120+
def upload_file():
121+
if request.method == "POST":
122+
if "file" not in request.files:
123+
return "没有文件部分"
124+
file = request.files["file"]
125+
if file.filename == "":
126+
return "没有选择文件"
127+
if file:
128+
filename = file.filename
129+
file.save(os.path.join(app.config["UPLOAD_FOLDER"], filename))
130+
return "文件上传成功"
131+
return render_template("upload.html")
132+
133+
@app.route("/uploads/<filename>")
134+
def uploaded_file(filename):
135+
return send_from_directory(app.config["UPLOAD_FOLDER"], filename)
34136

35137
class Audio:
36138
def __init__(self):
@@ -75,34 +177,60 @@ class Audio:
75177
return self.wav_header + data
76178

77179
def _get_audio_subsequent_times(self):
78-
return self.stream.read(self.CHUNK)
180+
if self.stream:
181+
data = self.stream.read(self.CHUNK)
182+
shared_timestamp.update() # 更新时间戳
183+
return data
184+
else:
185+
return b'\x00' * self.CHUNK * 2 # 返回静音数据
186+
187+
class SharedTimestamp:
188+
def __init__(self):
189+
self.timestamp = 0
190+
self.lock = threading.Lock()
191+
192+
def update(self):
193+
with self.lock:
194+
self.timestamp = time.time()
195+
196+
def get(self):
197+
with self.lock:
198+
return self.timestamp
199+
200+
shared_timestamp = SharedTimestamp()
79201

80202
class CameraEvent(object):
81203
def __init__(self):
82204
self.events = {}
205+
self.lock = threading.Lock()
83206

84207
def wait(self):
85208
ident = get_ident()
86-
if ident not in self.events:
87-
self.events[ident] = [threading.Event(), time.time()]
88-
return self.events[ident][0].wait()
209+
with self.lock:
210+
if ident not in self.events:
211+
self.events[ident] = [threading.Event(), time.time()]
212+
event = self.events[ident][0]
213+
event.wait()
89214

90215
def set(self):
91216
now = time.time()
92-
remove = None
93-
for ident, event in self.events.items():
94-
if not event[0].is_set():
95-
event[0].set()
96-
event[1] = now
97-
else:
98-
if now - event[1] > 5:
99-
remove = ident
100-
if remove:
101-
del self.events[remove]
217+
with self.lock:
218+
remove = []
219+
for ident, event in self.events.items():
220+
if not event[0].is_set():
221+
event[0].set()
222+
event[1] = now
223+
else:
224+
if now - event[1] > 5:
225+
remove.append(ident)
226+
for ident in remove:
227+
del self.events[ident]
102228

103229
def clear(self):
104-
self.events[get_ident()][0].clear()
105-
230+
ident = get_ident()
231+
with self.lock:
232+
if ident in self.events:
233+
self.events[ident][0].clear()
106234

107235
class BaseCamera(object):
108236
thread = None
@@ -131,100 +259,115 @@ class BaseCamera(object):
131259
@classmethod
132260
def _thread(cls):
133261
print("Starting camera thread.")
134-
frames_iterator = cls.frames()
135-
for frame in frames_iterator:
136-
BaseCamera.frame = frame
137-
BaseCamera.event.set()
138-
time.sleep(0)
139-
if time.time() - BaseCamera.last_access > 10:
140-
frames_iterator.close()
141-
print("Stopping camera thread due to inactivity.")
142-
break
143-
BaseCamera.thread = None
262+
try:
263+
frames_iterator = cls.frames()
264+
for frame in frames_iterator:
265+
BaseCamera.frame = frame
266+
shared_timestamp.update() # 更新时间戳
267+
BaseCamera.event.set()
268+
time.sleep(0)
269+
if time.time() - BaseCamera.last_access > 10:
270+
frames_iterator.close()
271+
print("Stopping camera thread due to inactivity.")
272+
break
273+
except Exception as e:
274+
print(f"Camera thread encountered an error: {e}")
275+
finally:
276+
BaseCamera.thread = None
277+
cls.release_resources()
144278

279+
@classmethod
280+
def release_resources(cls):
281+
if hasattr(cls, 'cap') and cls.cap.isOpened():
282+
cls.cap.release()
283+
print("Camera resources released.")
145284

146285
class Camera(BaseCamera):
147-
video_source = 0
286+
cap = None
148287

149-
@staticmethod
150-
def set_video_source(source):
151-
Camera.video_source = source
288+
def __init__(self):
289+
if Camera.cap is None:
290+
Camera.cap = cv2.VideoCapture(0)
291+
if not Camera.cap.isOpened():
292+
logging.error("无法打开摄像头")
293+
raise RuntimeError("无法打开摄像头")
294+
super().__init__()
152295

153296
@staticmethod
154297
def frames():
155-
camera = cv2.VideoCapture(Camera.video_source)
156-
if not camera.isOpened():
157-
raise RuntimeError("Error")
158298
while True:
159-
image = ImageGrab.grab()
160-
image = image.resize((1366, 750), Image.LANCZOS)
161-
output_buffer = BytesIO()
162-
image.save(output_buffer, format="JPEG", quality=100)
163-
frame = output_buffer.getvalue()
164-
yield frame
165-
app = Flask(__name__)
299+
if Camera.cap is None:
300+
Camera.cap = cv2.VideoCapture(0)
301+
success, frame = Camera.cap.read()
302+
if not success:
303+
logging.error("无法从摄像头读取帧")
304+
break
305+
else:
306+
ret, jpeg = cv2.imencode('.jpg', frame)
307+
if not ret:
308+
logging.error("无法编码图像")
309+
continue
310+
yield jpeg.tobytes()
166311

167-
def gen(camera):
312+
@classmethod
313+
def release_resources(cls):
314+
if cls.cap is not None:
315+
cls.cap.release()
316+
cls.cap = None
317+
logging.info("摄像头资源已释放")
318+
319+
class DesktopCapture(BaseCamera):
320+
@staticmethod
321+
def frames():
322+
while True:
323+
screen = np.array(ImageGrab.grab())
324+
frame = cv2.cvtColor(screen, cv2.COLOR_RGB2BGR)
325+
ret, jpeg = cv2.imencode('.jpg', frame)
326+
if not ret:
327+
logging.error("无法编码桌面图像")
328+
continue
329+
yield jpeg.tobytes()
330+
331+
def gen_video(camera):
168332
while True:
169333
frame = camera.get_frame()
170-
yield (b"--frame\r\n" b"Content-Type: image/jpeg\r\n\r\n" + frame + b"\r\n")
334+
timestamp = shared_timestamp.get()
335+
yield frame
171336

172337
def gen_audio(audio):
173338
while True:
174339
data = audio.get_audio()
340+
timestamp = shared_timestamp.get()
175341
yield data
176342

177-
@app.route("/video_feed")
178-
def video_feed():
179-
return Response(gen(Camera()), mimetype="multipart/x-mixed-replace; boundary=frame")
343+
@app.route("/c_video")
344+
def camera_video_feed():
345+
return Response(gen_video(Camera()))
346+
347+
@app.route("/d_video")
348+
def desktop_video_feed():
349+
return Response(gen_video(DesktopCapture()))
180350

181351
@app.route("/audio_feed")
182352
def audio_feed():
183-
return Response(stream_with_context(gen_audio(Audio())))
184-
185-
@app.route("/")
186-
def index():
187-
global mode
188-
video_tag = """<img src="{{ url_for('video_feed') }}">"""
189-
audio_tag = """<audio autoplay style="display:none;"><source src="{{ url_for('audio_feed') }}" type="audio/x-wav; codec=pcm">Your browser does not support the audio element.</audio>"""
190-
191-
tags = {0: video_tag + audio_tag, 1: audio_tag, 2: video_tag}
192-
193-
content = tags[mode]
194-
195-
return render_template_string(
196-
"""<html>
197-
<head>
198-
<title>{title}</title>
199-
<link rel="icon" href="data:image/svg+xml;base64,CjxzdmcgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB2aWV3Qm94PSIwIDAgNTAgNTAiPgogICAgPGNpcmNsZSBjeD0iMjUiIGN5PSIyNSIgcj0iMjAiIGZpbGw9InJlZCIgLz4KPC9zdmc+Cg==">
200-
</head>
201-
<body>{content}</body>
202-
</html>""".format(
203-
title=["Intranet Broadcast", "Audio Sharing", "Screen Sharing"][mode],
204-
content=content,
205-
)
206-
)
207-
208-
if __name__ == "__main__":
209-
local_host = "127.0.0.1"
210-
ip_host = "0.0.0.0"
211-
port = 8001
212-
mode = int(input("Please select the mode: 0 for Intranet Broadcast, 1 for Audio Sharing, 2 for Screen Sharing: "))
213-
app.run(threaded=True, host=ip_host, port=port)
353+
return Response(gen_audio(Audio()))
214354

215-
```
216-
运行程序后,程序会提示你输入一个数字:
217-
218-
0表示同时分享屏幕和音频
219-
1表示仅分享音频
220-
2表示仅分享屏幕
221-
222-
输入相应数字后按回车键即可。程序运行后,会在控制台输出一个URL。你只需在浏览器中输入这个URL,就可以看到你的屏幕和音频了。
355+
@app.route("/c")
356+
def camera_page():
357+
return render_template("stream.html", video_url="/c_video", audio_url="/audio_feed")
223358

224-
## 后话
359+
@app.route("/d")
360+
def desktop_page():
361+
return render_template("stream.html", video_url="/d_video", audio_url="/audio_feed")
225362

226-
你可以在此项目的基础上进行扩展,增加更多功能,如:
227-
228-
- [x] 识别当前音频并将其转化为文本,与屏幕共享一起传输,这样观众就可以在屏幕上看到你的讲话内容。
229-
- [x] 结合翻译API,实现实时翻译功能。
230-
- [x] 压缩屏幕画面质量,获得更流畅的传输效果等等。
363+
if __name__ == "__main__":
364+
try:
365+
os.makedirs(UPLOAD_FOLDER, exist_ok=True)
366+
local_host = "127.0.0.1"
367+
ip_host = "0.0.0.0"
368+
port = 8001
369+
app.run(threaded=True, host=ip_host, port=port, debug=True)
370+
except Exception as e:
371+
logging.error(f"发生错误: {e}")
372+
# 可以选择在这里保持程序运行
373+
```

0 commit comments

Comments
 (0)