기능 플로우 정리
- 사용자1은 화면 공유 버튼을 누르면, 다른 화면으로 전환된다.
- 해당 화면에는 실시간으로 사용자1의 폰 화면이 보여야 한다.
- 상단에 작은 박스로 검은색 투명도 50%인 박스가 있어야 한다.
- 이 박스의 오른쪽에는 화면 공유 종료 버튼이 있어야 한다.
개발 환경
Front End : Flutter
Back End : FastAPI
내가 직접 API를 만들어볼거야! 라고 시도해봤지만,
결론적으로 어떤 문제인지 모르고 망한 코드인 점을 고려하고, 참고만 부탁드립니다!
해결방법이 있다면, 댓글로 공유해주시면 감사하겠습니다 ,,
- main.py
from fastapi import FastAPI, WebSocket
from fastapi.responses import StreamingResponse
import cv2
import numpy as np
app = FastAPI()
# 사용자1과 사용자2의 화면 공유 상태를 저장하는 딕셔너리
screen_sharing_states = {
"user1": False,
"user2": False
}
# 웹 소켓 연결을 위한 딕셔너리
sockets = {}
# 웹 소켓 연결
@app.websocket("/ws/{client_id}")
async def websocket_endpoint(websocket: WebSocket, client_id: str):
await websocket.accept()
sockets[client_id] = websocket
try:
while True:
data = await websocket.receive_text()
# 클라이언트로부터의 메시지 처리 (필요시 구현)
except:
del sockets[client_id]
# 화면 공유를 위한 라우트
@app.get("/stream/{client_id}")
async def stream_view(client_id: str):
if screen_sharing_states.get(client_id, False):
return StreamingResponse(stream_generator(client_id), media_type="multipart/x-mixed-replace;boundary=frame")
else:
return {"message": "Screen sharing is not active"}
# 화면 캡처 및 스트리밍
def stream_generator(client_id):
cap = cv2.VideoCapture(0)
while True:
ret, frame = cap.read()
if ret:
frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
_, img_encoded = cv2.imencode(".jpg", frame)
yield (
b"--frame\\r\\n"
b"Content-Type: image/jpeg\\r\\n\\r\\n" + img_encoded.tobytes() + b"\\r\\n"
)
cap.release()
# 사용자1이 화면 공유 버튼을 누를 때 호출될 함수
@app.post("/start_screen_sharing/{client_id}")
async def start_screen_sharing(client_id: str):
screen_sharing_states[client_id] = True
return {"message": f"Screen sharing started for {client_id}"}
# 사용자1이 화면 공유 종료 버튼을 누를 때 호출될 함수
@app.post("/stop_screen_sharing/{client_id}")
async def stop_screen_sharing(client_id: str):
screen_sharing_states[client_id] = False
return {"message": f"Screen sharing stopped for {client_id}"}
# FastAPI 서버 실행
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8000)
DB가 없더라도 테스트를 위해 간단한 데이터를 저장하는 방법 ?
- 일반적으로 개발 및 테스트 목적으로 데이터베이스를 대체하기 위해 메모리 내 데이터 구조 또는 파일을 사용할 수 있다. 예를 들어 Python에서는 리스트, 딕셔너리, 또는 파일을 사용하여 데이터를 저장할 수 있다.
- 이 코드에서는 screen_sharing_states 딕셔너리를 사용하여 사용자1과 사용자2의 화면 공유 상태를 저장한다.
- 사용자1과 사용자2의 화면 공유 상태를 메모리 내 딕셔너리를 사용하여 관리합니다.
- 이 상태는 메모리 내에 저장되며, 데이터베이스 대신 사용된다.
- 테스트를 위해 이 딕셔너리를 변경하고, 사용자1 및 사용자2의 화면 공유 상태가 변경되었음을 확인할 수 있다.
정리
- FastAPI
- Python으로 빠르고 현대적인 웹 애플리케이션을 빌드하기 위한 웹 프레임워크
- 비동기 및 형식 기반(타입 힌팅을 사용하여 입력 및 출력 데이터를 정의)의 기능을 제공하여 개발자가 빠르고 안정적인 웹 서비스를 작성할 수 있도록 한다.
- WebSocket
- WebSocket은 TCP 기반의 양방향 통신 프로토콜
- 클라이언트와 서버 간에 실시간으로 데이터를 주고받을 수 있도록 지원한다.
- 이 프로토콜을 사용하면 클라이언트와 서버 간의 실시간 데이터 전송이 가능하며, 양쪽 모두에서 언제든지 데이터를 보낼 수 있다.
- StreamingResponse
- 비동기적으로 데이터를 전송하는 데 사용되는 FastAPI의 응답 형식 중 하나
- 이를 사용하면 대용량의 데이터를 조각 조각으로 전송할 수 있으며, 데이터가 생성될 때마다 클라이언트로 전송할 수 있다.
- 위의 코드에서는 OpenCV를 사용하여 카메라 영상을 실시간으로 캡처하고 JPEG 이미지로 인코딩한 후, StreamingResponse를 통해 클라이언트로 전송한다.
- cv2.VideoCapture
- OpenCV의 기능 중 하나
- 카메라로부터 비디오를 캡처하기 위해 사용된다.
- 이를 통해 카메라로부터 비디오 프레임을 읽어올 수 있다.
- uvicorn
- ASGI(Asynchronous Server Gateway Interface) 서버를 실행하기 위한 간단하고 빠른 서버
- FastAPI 애플리케이션을 ASGI 서버로 실행하기 위해 사용된다.
- 위의 코드에서는 uvicorn을 사용하여 FastAPI 애플리케이션을 호스팅하고, host 및 port 매개변수를 사용하여 서버가 실행될 호스트 및 포트를 지정한다.
- 라우트 (Route)
- 이 API는 웹 애플리케이션에서 특정 URL에 대한 요청을 처리하는 엔드포인트를 정의한다.
- @app.get("/stream/{client_id}")는 HTTP GET 요청이 "/stream/{client_id}" 경로로 전송될 때 호출되는 핸들러 함수를 정의한다.
- {client_id}는 경로 매개변수로, 클라이언트에 따라 다른 값을 가질 수 있다.
- 비동기 함수 (Async Function):
- 이 API는 비동기 함수로 정의되어 있습니다. 따라서 async def 키워드로 시작한다.
- 비동기 함수를 사용하면 실행 흐름을 차단하지 않고 비동기적으로 작업을 수행할 수 있다.
- 스트리밍 응답 (Streaming Response):
- 이 API는 StreamingResponse 객체를 반환하여 클라이언트에게 데이터를 스트리밍하는 방법을 제공한다.
- 클라이언트가 "/stream/{client_id}"에 대한 GET 요청을 보내면, 서버는 stream_view 함수를 호출하고, 이 함수는 StreamingResponse를 반환한다.
- StreamingResponse는 대용량 데이터를 클라이언트로 보낼 때 사용됩니다. 여기서는 OpenCV를 사용하여 카메라 영상을 실시간으로 캡처하고 JPEG 이미지로 인코딩하여 클라이언트로 전송한다.
- 미디어 타입 (Media Type):
- media_type="multipart/x-mixed-replace;boundary=frame"은 클라이언트에게 전송되는 데이터의 미디어 타입을 정의한다.
- 여기서는 "multipart/x-mixed-replace" 타입을 사용하여 여러 이미지를 하나의 응답에 포함시킨다. "boundary=frame"은 각 이미지의 경계를 나타내는 문자열이다.
- main.dart
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'package:web_socket_channel/io.dart';
import 'package:web_socket_channel/status.dart' as status;
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Screen Sharing App',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
@override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
final channel = IOWebSocketChannel.connect('ws://127.0.0.1:8000/ws/user2');
String _imageURL = '<http://127.0.0.1:8000/stream/user1>';
bool _screenSharingActive = false;
void _startScreenSharing() async {
final url = Uri.parse('<http://127.0.0.1:8000/start_screen_sharing>');
final response = await http.post(url);
if (response.statusCode == 200) {
setState(() {
_screenSharingActive = true;
});
} else {
print('Failed to start screen sharing');
}
}
void _stopScreenSharing() async {
final url = Uri.parse('<http://127.0.0.1:8000/stop_screen_sharing>');
final response = await http.post(url);
if (response.statusCode == 200) {
setState(() {
_screenSharingActive = false;
});
} else {
print('Failed to stop screen sharing');
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Screen Sharing App'),
),
body: Stack(
children: [
Center(
child: _screenSharingActive
? StreamBuilder(
stream: channel.stream,
builder: (context, snapshot) {
if (snapshot.hasData) {
return Image.network(_imageURL);
} else if (snapshot.hasError) {
return Text('Error: ${snapshot.error}');
} else {
return CircularProgressIndicator();
}
},
)
: Text('Screen sharing is not active'),
),
Positioned(
top: 0,
right: 0,
child: Container(
padding: EdgeInsets.all(8),
color: Colors.black.withOpacity(0.5),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
if (_screenSharingActive)
ElevatedButton(
onPressed: _stopScreenSharing,
child: Text('Stop Sharing'),
),
],
),
),
),
],
),
floatingActionButton: FloatingActionButton(
onPressed: _screenSharingActive ? null : _startScreenSharing,
child: Icon(Icons.screen_share),
),
);
}
@override
void dispose() {
channel.sink.close(status.goingAway);
super.dispose();
}
}
오류 및 실패
"Connection refused"
- 오류는 클라이언트가 서버와의 연결을 시도할 때 연결이 거부되었음을 나타낸다.
- 이 오류는 주로 서버가 제대로 실행되지 않거나 클라이언트가 잘못된 주소 또는 포트로 연결을 시도할 때 발생한다.
주요 해결 방법
- 서버 실행 확인
- 먼저 FastAPI 서버가 제대로 실행 중인지 확인
- 서버가 실행 중이지 않은 경우에는 해당 서버를 실행하고 있는지 확인해야 한다.
- 서버 주소 및 포트 확인
- 클라이언트 코드에서 서버의 주소 및 포트가 올바른지 확인
- 주소와 포트가 정확해야 서버와의 연결이 가능하다.
- 방화벽 확인
- 방화벽이 클라이언트와 서버 간의 통신을 방해하고 있는지 확인
- 방화벽 설정을 확인하고 필요한 경우 포트를 열어야 한다.
- 네트워크 연결 확인
- 클라이언트와 서버가 동일한 네트워크에 연결되어 있는지 확인
- 동일한 네트워크에 연결되어 있지 않은 경우에는 서버에 접근할 수 없다.
결론
- 위의 4가지 방법을 다 진행했지만, 오류는 해결되지 않았다.
- 시간적으로도 부족하기도 하고, 내가 만든 API말고 사이트( 실시간 화면 공유(Flutter) | Tencent Cloud )에 잘 나와있어서 갈아엎을려고 한다.
Tencent Cloud
Tencent is a leading influencer in industries such as social media, mobile payments, online video, games, music, and more. Leverage Tencent's vast ecosystem of key products across various verticals as well as its extensive expertise and networks to gain a
www.tencentcloud.com
혹시 4가지 방법으로 안되는 이유를 아시거나,
서버 연결 오류 해결 방법을 아시는 분은 댓글주시면 감사하겠습니다!
'Flutter' 카테고리의 다른 글
Push 알림 및 FCM 설정 (0) | 2024.04.25 |
---|---|
Flutter 개발 환경 구축 및 단점 (0) | 2024.03.25 |