@@ -10,12 +10,18 @@ import GRPC
10
10
import NIO
11
11
import Foundation
12
12
import PhotosUI
13
+ import AVFoundation
14
+ import Combine
15
+ import AVKit
13
16
14
17
// GRPC 채널 생성 예시
15
18
16
- class VideoStreamService {
19
+ class VideoStreamService : ObservableObject {
17
20
private var client : FDFireDetectionServiceNIOClient ?
18
21
private var streamCall : BidirectionalStreamingCall < FDVideoChunk , FDVideoResponse > ?
22
+ var player : AVPlayer ?
23
+ @Published var isPlaying = false
24
+ @Published var fireDetected = false
19
25
20
26
init ( ) {
21
27
setupGRPC ( )
@@ -42,60 +48,168 @@ class VideoStreamService {
42
48
return
43
49
}
44
50
51
+ // 비디오 에셋 설정
52
+ let asset = AVAsset ( url: fileURL)
53
+
54
+ // 플레이어 설정
55
+ player = AVPlayer ( url: fileURL)
56
+
45
57
// 서버로부터의 응답을 처리할 스트림 콜 설정
46
58
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
+ }
50
67
}
51
68
}
52
69
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
58
105
}
59
106
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
67
113
}
68
114
69
- print ( " .. " )
70
- try streamCall ? . sendMessage ( chunk )
71
-
115
+ guard let imageBuffer = CMSampleBufferGetImageBuffer ( sampleBuffer ) else {
116
+ continue
117
+ }
72
118
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 }
74
135
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
77
145
}
78
146
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
+ }
81
158
}
82
159
}
83
160
84
161
func stopStreaming( ) {
162
+ player? . pause ( )
85
163
try ? streamCall? . sendEnd ( )
164
+ isPlaying = false
86
165
}
87
166
}
88
167
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
+
89
180
struct ConnectView : View {
90
- private let streamService = VideoStreamService ( )
181
+ @ StateObject private var streamService = VideoStreamService ( )
91
182
92
183
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
+ }
96
203
}
204
+ . padding ( )
205
+ . background ( streamService. isPlaying ? Color . red : Color . blue)
206
+ . foregroundColor ( . white)
207
+ . cornerRadius ( 8 )
97
208
}
209
+ . padding ( )
98
210
}
99
211
}
100
212
101
-
213
+ #Preview {
214
+ ConnectView ( )
215
+ }
0 commit comments