Skip to content

Commit a11e633

Browse files
committed
feat - #13 Video Player UI & Networking
1 parent 8f0daa2 commit a11e633

File tree

14 files changed

+869
-115
lines changed

14 files changed

+869
-115
lines changed

.DS_Store

0 Bytes
Binary file not shown.

HomeCamProject/.DS_Store

0 Bytes
Binary file not shown.

HomeCamProject/HomeCamProject.xcodeproj/project.pbxproj

+4-8
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,7 @@
1717
91B8B0312CF83D9200365904 /* ConnectView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 91B8B0302CF83D9200365904 /* ConnectView.swift */; };
1818
91B8B0352CF8454F00365904 /* fire_detection.grpc.swift in Sources */ = {isa = PBXBuildFile; fileRef = 91B8B0322CF8454F00365904 /* fire_detection.grpc.swift */; };
1919
91B8B0362CF8454F00365904 /* fire_detection.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = 91B8B0332CF8454F00365904 /* fire_detection.pb.swift */; };
20-
91B8B03B2CF85D5400365904 /* sample.mp4 in Resources */ = {isa = PBXBuildFile; fileRef = 91B8B03A2CF85D5400365904 /* sample.mp4 */; };
21-
D61122132CFA9BE20040AD8C /* VideoContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61122122CFA9BE20040AD8C /* VideoContentView.swift */; };
20+
D611221B2CFAB0AF0040AD8C /* sample.mp4 in Resources */ = {isa = PBXBuildFile; fileRef = D611221A2CFAB0AF0040AD8C /* sample.mp4 */; };
2221
/* End PBXBuildFile section */
2322

2423
/* Begin PBXFileReference section */
@@ -30,8 +29,7 @@
3029
91B8B0302CF83D9200365904 /* ConnectView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectView.swift; sourceTree = "<group>"; };
3130
91B8B0322CF8454F00365904 /* fire_detection.grpc.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = fire_detection.grpc.swift; sourceTree = "<group>"; };
3231
91B8B0332CF8454F00365904 /* fire_detection.pb.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = fire_detection.pb.swift; sourceTree = "<group>"; };
33-
91B8B03A2CF85D5400365904 /* sample.mp4 */ = {isa = PBXFileReference; lastKnownFileType = file; path = sample.mp4; sourceTree = "<group>"; };
34-
D61122122CFA9BE20040AD8C /* VideoContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoContentView.swift; sourceTree = "<group>"; };
32+
D611221A2CFAB0AF0040AD8C /* sample.mp4 */ = {isa = PBXFileReference; lastKnownFileType = file; path = sample.mp4; sourceTree = "<group>"; };
3533
/* End PBXFileReference section */
3634

3735
/* Begin PBXFrameworksBuildPhase section */
@@ -71,9 +69,8 @@
7169
91B2911F2CF81CD60035D87D /* HomeCamProjectApp.swift */,
7270
91B291212CF81CD60035D87D /* ContentView.swift */,
7371
91B8B0302CF83D9200365904 /* ConnectView.swift */,
74-
D61122122CFA9BE20040AD8C /* VideoContentView.swift */,
7572
91B291232CF81CD80035D87D /* Assets.xcassets */,
76-
91B8B03A2CF85D5400365904 /* sample.mp4 */,
73+
D611221A2CFAB0AF0040AD8C /* sample.mp4 */,
7774
91B291252CF81CD80035D87D /* Preview Content */,
7875
);
7976
path = HomeCamProject;
@@ -164,7 +161,7 @@
164161
buildActionMask = 2147483647;
165162
files = (
166163
91B291272CF81CD80035D87D /* Preview Assets.xcassets in Resources */,
167-
91B8B03B2CF85D5400365904 /* sample.mp4 in Resources */,
164+
D611221B2CFAB0AF0040AD8C /* sample.mp4 in Resources */,
168165
91B291242CF81CD80035D87D /* Assets.xcassets in Resources */,
169166
);
170167
runOnlyForDeploymentPostprocessing = 0;
@@ -180,7 +177,6 @@
180177
91B291222CF81CD60035D87D /* ContentView.swift in Sources */,
181178
91B8B0352CF8454F00365904 /* fire_detection.grpc.swift in Sources */,
182179
91B291202CF81CD60035D87D /* HomeCamProjectApp.swift in Sources */,
183-
D61122132CFA9BE20040AD8C /* VideoContentView.swift in Sources */,
184180
91B8B0362CF8454F00365904 /* fire_detection.pb.swift in Sources */,
185181
);
186182
runOnlyForDeploymentPostprocessing = 0;

HomeCamProject/HomeCamProject/ConnectView.swift

+143-29
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,18 @@ import GRPC
1010
import NIO
1111
import Foundation
1212
import PhotosUI
13+
import AVFoundation
14+
import Combine
15+
import AVKit
1316

1417
// GRPC 채널 생성 예시
1518

16-
class VideoStreamService {
19+
class VideoStreamService: ObservableObject {
1720
private var client: FDFireDetectionServiceNIOClient?
1821
private var streamCall: BidirectionalStreamingCall<FDVideoChunk, FDVideoResponse>?
22+
var player: AVPlayer?
23+
@Published var isPlaying = false
24+
@Published var fireDetected = false
1925

2026
init() {
2127
setupGRPC()
@@ -42,60 +48,168 @@ class VideoStreamService {
4248
return
4349
}
4450

51+
// 비디오 에셋 설정
52+
let asset = AVAsset(url: fileURL)
53+
54+
// 플레이어 설정
55+
player = AVPlayer(url: fileURL)
56+
4557
// 서버로부터의 응답을 처리할 스트림 콜 설정
4658
streamCall = client.streamVideo { response in
47-
print("Received response from server: \(response.message)")
48-
if response.detected {
49-
print("Fire detected at timestamp: \(response.timestamp)")
59+
DispatchQueue.main.async {
60+
if response.detected {
61+
self.fireDetected = true
62+
print("🔥 Fire detected at timestamp: \(response.timestamp)")
63+
} else {
64+
self.fireDetected = false
65+
print(". . . Streaming . . .")
66+
}
5067
}
5168
}
5269

53-
do {
54-
let videoData = try Data(contentsOf: fileURL)
55-
let chunkSize = 1024 * 1024 // 1MB 단위로 청크 생성
56-
let chunks = stride(from: 0, to: videoData.count, by: chunkSize).map {
57-
videoData[$0..<min($0 + chunkSize, videoData.count)]
70+
// 비디오 프레임 추출 설정
71+
guard let reader = try? AVAssetReader(asset: asset),
72+
let videoTrack = asset.tracks(withMediaType: .video).first else { // 비디오 트랙을 가져오는 부분
73+
print("Failed to create asset reader")
74+
return
75+
}
76+
77+
let outputSettings: [String: Any] = [
78+
kCVPixelBufferPixelFormatTypeKey as String: kCVPixelFormatType_32BGRA
79+
]
80+
81+
let readerOutput = AVAssetReaderTrackOutput(
82+
track: videoTrack,
83+
outputSettings: outputSettings
84+
)
85+
reader.add(readerOutput)
86+
87+
// FPS 계산
88+
let fps = videoTrack.nominalFrameRate
89+
let frameDuration = 1.0 / Double(fps)
90+
91+
// 프레임 전송 및 재생 시작
92+
reader.startReading()
93+
player?.play()
94+
isPlaying = true
95+
96+
let startTime = Date()
97+
var frameCount = 0
98+
// 백그라운드 큐에서 프레임 처리
99+
DispatchQueue.global(qos: .userInitiated).async { [weak self] in
100+
// reader 상태 체크 추가
101+
guard reader.status == .reading else {
102+
print("Reader is not in reading state")
103+
self?.stopStreaming()
104+
return
58105
}
59106

60-
// 각 청크를 순차적으로 전송
61-
for (index, chunkData) in chunks.enumerated() {
62-
let isLast = index == chunks.count - 1
63-
let chunk = FDVideoChunk.with {
64-
$0.data = chunkData
65-
$0.timestamp = Int64(Date().timeIntervalSince1970 * 1000)
66-
$0.isLast = isLast
107+
while let sampleBuffer = readerOutput.copyNextSampleBuffer() {
108+
// reader 상태 지속적 체크
109+
if reader.status != .reading {
110+
print("Reader status changed: \(reader.status)")
111+
self?.stopStreaming()
112+
break
67113
}
68114

69-
print("..")
70-
try streamCall?.sendMessage(chunk)
71-
115+
guard let imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) else {
116+
continue
117+
}
72118

73-
print("Sent chunk \(index) of size \(chunkData.count) bytes.")
119+
// 타이밍 동기화
120+
let expectedTime = startTime.addingTimeInterval(Double(frameCount) * frameDuration)
121+
let delay = expectedTime.timeIntervalSinceNow
122+
if delay > 0 {
123+
Thread.sleep(forTimeInterval: delay)
124+
}
125+
126+
// JPEG 인코딩
127+
let ciImage = CIImage(cvImageBuffer: imageBuffer)
128+
let context = CIContext()
129+
guard let colorSpace = CGColorSpace(name: CGColorSpace.sRGB),
130+
let jpegData = context.jpegRepresentation(
131+
of: ciImage,
132+
colorSpace: colorSpace,
133+
options: [kCGImageDestinationLossyCompressionQuality as CIImageRepresentationOption: 0.7]
134+
) else { continue }
74135

75-
// 청크 간 약간의 딜레이를 줄 수 있습니다
76-
Thread.sleep(forTimeInterval: 0.1)
136+
// 프레임 전송
137+
let chunk = FDVideoChunk.with {
138+
$0.data = jpegData
139+
$0.timestamp = Int64(Date().timeIntervalSince1970 * 1000)
140+
$0.isLast = false
141+
}
142+
143+
try? self?.streamCall?.sendMessage(chunk)
144+
frameCount += 1
77145
}
78146

79-
} catch {
80-
print("Error streaming video: \(error)")
147+
// 스트림 종료
148+
let finalChunk = FDVideoChunk.with {
149+
$0.isLast = true
150+
$0.timestamp = Int64(Date().timeIntervalSince1970 * 1000)
151+
}
152+
try? self?.streamCall?.sendMessage(finalChunk)
153+
try? self?.streamCall?.sendEnd()
154+
155+
DispatchQueue.main.async {
156+
self?.isPlaying = false
157+
}
81158
}
82159
}
83160

84161
func stopStreaming() {
162+
player?.pause()
85163
try? streamCall?.sendEnd()
164+
isPlaying = false
86165
}
87166
}
88167

168+
struct VideoPlayerView: UIViewControllerRepresentable {
169+
let player: AVPlayer
170+
171+
func makeUIViewController(context: Context) -> AVPlayerViewController {
172+
let controller = AVPlayerViewController()
173+
controller.player = player
174+
return controller
175+
}
176+
177+
func updateUIViewController(_ uiViewController: AVPlayerViewController, context: Context) {}
178+
}
179+
89180
struct ConnectView: View {
90-
private let streamService = VideoStreamService()
181+
@StateObject private var streamService = VideoStreamService()
91182

92183
var body: some View {
93-
Button("Start Streaming") {
94-
if let videoUrl = Bundle.main.url(forResource: "sample", withExtension: "mp4") {
95-
streamService.streamVideo(fileURL: videoUrl)
184+
VStack {
185+
if streamService.isPlaying,
186+
let player = streamService.player {
187+
VideoPlayerView(player: player)
188+
.frame(height: 300)
189+
}
190+
191+
if streamService.fireDetected {
192+
Text("🔥 Fire Detected!")
193+
.foregroundColor(.red)
194+
.font(.headline)
195+
}
196+
197+
Button(streamService.isPlaying ? "Stop Streaming" : "Start Streaming") {
198+
if streamService.isPlaying {
199+
streamService.stopStreaming()
200+
} else if let videoUrl = Bundle.main.url(forResource: "sample", withExtension: "mp4") {
201+
streamService.streamVideo(fileURL: videoUrl)
202+
}
96203
}
204+
.padding()
205+
.background(streamService.isPlaying ? Color.red : Color.blue)
206+
.foregroundColor(.white)
207+
.cornerRadius(8)
97208
}
209+
.padding()
98210
}
99211
}
100212

101-
213+
#Preview {
214+
ConnectView()
215+
}

HomeCamProject/HomeCamProject/HomeCamProjectApp.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import GRPC
1212
struct HomeCamProjectApp: App {
1313
var body: some Scene {
1414
WindowGroup {
15-
VideoContentView()
15+
ConnectView()
1616
}
1717
}
1818
}

HomeCamProject/HomeCamProject/VideoContentView.swift

-75
This file was deleted.
7.82 MB
Binary file not shown.

model_server/Pipfile

+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
[[source]]
2+
url = "https://pypi.org/simple"
3+
verify_ssl = true
4+
name = "pypi"
5+
6+
[packages]
7+
grpcio = "*"
8+
grpcio-tools = "*"
9+
torch = "*"
10+
opencv-python = "*"
11+
pandas = "*"
12+
numpy = "*"
13+
requests = "*"
14+
pillow = "*"
15+
16+
[dev-packages]
17+
18+
[requires]
19+
python_version = "3.12"
20+
python_full_version = "3.12.5"

0 commit comments

Comments
 (0)