@@ -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
2692import time
2793import cv2
28- from PIL import ImageGrab, Image
94+ import numpy as np
95+ from PIL import ImageGrab
2996import threading
30- from io import BytesIO
3197from greenlet import getcurrent as get_ident
3298import 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
35137class 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
80202class 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
107235class 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
146285class 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
172337def 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" )
182352def 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