Skip to content

Commit c404b9b

Browse files
authored
Merge pull request #15 from GDG-on-Campus-KHU/feat/#13
feat - #13 Video Player UI & Networking
2 parents 83d2070 + a11e633 commit c404b9b

File tree

17 files changed

+996
-36
lines changed

17 files changed

+996
-36
lines changed

.DS_Store

8 KB
Binary file not shown.

HomeCamProject/.DS_Store

2 KB
Binary file not shown.

HomeCamProject/HomeCamProject.xcodeproj/project.pbxproj

+4-4
Original file line numberDiff line numberDiff line change
@@ -17,7 +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 */; };
20+
D611221B2CFAB0AF0040AD8C /* sample.mp4 in Resources */ = {isa = PBXBuildFile; fileRef = D611221A2CFAB0AF0040AD8C /* sample.mp4 */; };
2121
/* End PBXBuildFile section */
2222

2323
/* Begin PBXFileReference section */
@@ -29,7 +29,7 @@
2929
91B8B0302CF83D9200365904 /* ConnectView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectView.swift; sourceTree = "<group>"; };
3030
91B8B0322CF8454F00365904 /* fire_detection.grpc.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = fire_detection.grpc.swift; sourceTree = "<group>"; };
3131
91B8B0332CF8454F00365904 /* fire_detection.pb.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = fire_detection.pb.swift; sourceTree = "<group>"; };
32-
91B8B03A2CF85D5400365904 /* sample.mp4 */ = {isa = PBXFileReference; lastKnownFileType = file; path = sample.mp4; sourceTree = "<group>"; };
32+
D611221A2CFAB0AF0040AD8C /* sample.mp4 */ = {isa = PBXFileReference; lastKnownFileType = file; path = sample.mp4; sourceTree = "<group>"; };
3333
/* End PBXFileReference section */
3434

3535
/* Begin PBXFrameworksBuildPhase section */
@@ -70,7 +70,7 @@
7070
91B291212CF81CD60035D87D /* ContentView.swift */,
7171
91B8B0302CF83D9200365904 /* ConnectView.swift */,
7272
91B291232CF81CD80035D87D /* Assets.xcassets */,
73-
91B8B03A2CF85D5400365904 /* sample.mp4 */,
73+
D611221A2CFAB0AF0040AD8C /* sample.mp4 */,
7474
91B291252CF81CD80035D87D /* Preview Content */,
7575
);
7676
path = HomeCamProject;
@@ -161,7 +161,7 @@
161161
buildActionMask = 2147483647;
162162
files = (
163163
91B291272CF81CD80035D87D /* Preview Assets.xcassets in Resources */,
164-
91B8B03B2CF85D5400365904 /* sample.mp4 in Resources */,
164+
D611221B2CFAB0AF0040AD8C /* sample.mp4 in Resources */,
165165
91B291242CF81CD80035D87D /* Assets.xcassets in Resources */,
166166
);
167167
runOnlyForDeploymentPostprocessing = 0;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
{
2+
"originHash" : "b0d8975aa19f1bb7106ae78f24de3911e84189892afc3b6fe6db19bcec9e631c",
3+
"pins" : [
4+
{
5+
"identity" : "grpc-swift",
6+
"kind" : "remoteSourceControl",
7+
"location" : "https://github.com/grpc/grpc-swift.git",
8+
"state" : {
9+
"revision" : "8c5e99d0255c373e0330730d191a3423c57373fb",
10+
"version" : "1.24.2"
11+
}
12+
},
13+
{
14+
"identity" : "swift-atomics",
15+
"kind" : "remoteSourceControl",
16+
"location" : "https://github.com/apple/swift-atomics.git",
17+
"state" : {
18+
"revision" : "cd142fd2f64be2100422d658e7411e39489da985",
19+
"version" : "1.2.0"
20+
}
21+
},
22+
{
23+
"identity" : "swift-collections",
24+
"kind" : "remoteSourceControl",
25+
"location" : "https://github.com/apple/swift-collections.git",
26+
"state" : {
27+
"revision" : "671108c96644956dddcd89dd59c203dcdb36cec7",
28+
"version" : "1.1.4"
29+
}
30+
},
31+
{
32+
"identity" : "swift-http-types",
33+
"kind" : "remoteSourceControl",
34+
"location" : "https://github.com/apple/swift-http-types",
35+
"state" : {
36+
"revision" : "ef18d829e8b92d731ad27bb81583edd2094d1ce3",
37+
"version" : "1.3.1"
38+
}
39+
},
40+
{
41+
"identity" : "swift-log",
42+
"kind" : "remoteSourceControl",
43+
"location" : "https://github.com/apple/swift-log.git",
44+
"state" : {
45+
"revision" : "96a2f8a0fa41e9e09af4585e2724c4e825410b91",
46+
"version" : "1.6.2"
47+
}
48+
},
49+
{
50+
"identity" : "swift-nio",
51+
"kind" : "remoteSourceControl",
52+
"location" : "https://github.com/apple/swift-nio.git",
53+
"state" : {
54+
"revision" : "dca6594f65308c761a9c409e09fbf35f48d50d34",
55+
"version" : "2.77.0"
56+
}
57+
},
58+
{
59+
"identity" : "swift-nio-extras",
60+
"kind" : "remoteSourceControl",
61+
"location" : "https://github.com/apple/swift-nio-extras.git",
62+
"state" : {
63+
"revision" : "2e9746cfc57554f70b650b021b6ae4738abef3e6",
64+
"version" : "1.24.1"
65+
}
66+
},
67+
{
68+
"identity" : "swift-nio-http2",
69+
"kind" : "remoteSourceControl",
70+
"location" : "https://github.com/apple/swift-nio-http2.git",
71+
"state" : {
72+
"revision" : "eaa71bb6ae082eee5a07407b1ad0cbd8f48f9dca",
73+
"version" : "1.34.1"
74+
}
75+
},
76+
{
77+
"identity" : "swift-nio-ssl",
78+
"kind" : "remoteSourceControl",
79+
"location" : "https://github.com/apple/swift-nio-ssl.git",
80+
"state" : {
81+
"revision" : "c7e95421334b1068490b5d41314a50e70bab23d1",
82+
"version" : "2.29.0"
83+
}
84+
},
85+
{
86+
"identity" : "swift-nio-transport-services",
87+
"kind" : "remoteSourceControl",
88+
"location" : "https://github.com/apple/swift-nio-transport-services.git",
89+
"state" : {
90+
"revision" : "bbd5e63cf949b7db0c9edaf7a21e141c52afe214",
91+
"version" : "1.23.0"
92+
}
93+
},
94+
{
95+
"identity" : "swift-protobuf",
96+
"kind" : "remoteSourceControl",
97+
"location" : "https://github.com/apple/swift-protobuf.git",
98+
"state" : {
99+
"revision" : "ebc7251dd5b37f627c93698e4374084d98409633",
100+
"version" : "1.28.2"
101+
}
102+
},
103+
{
104+
"identity" : "swift-system",
105+
"kind" : "remoteSourceControl",
106+
"location" : "https://github.com/apple/swift-system.git",
107+
"state" : {
108+
"revision" : "c8a44d836fe7913603e246acab7c528c2e780168",
109+
"version" : "1.4.0"
110+
}
111+
}
112+
],
113+
"version" : 3
114+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3+
<plist version="1.0">
4+
<dict>
5+
<key>SchemeUserState</key>
6+
<dict>
7+
<key>HomeCamProject.xcscheme_^#shared#^_</key>
8+
<dict>
9+
<key>orderHint</key>
10+
<integer>0</integer>
11+
</dict>
12+
</dict>
13+
</dict>
14+
</plist>
0 Bytes
Binary file not shown.

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
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ import GRPC
1212
struct HomeCamProjectApp: App {
1313
var body: some Scene {
1414
WindowGroup {
15-
// ContentView()
1615
ConnectView()
1716
}
1817
}
7.82 MB
Binary file not shown.

0 commit comments

Comments
 (0)