I’m developing an iOS app using Swift that streams video content to a Roku device. I’m using GCDWebServer to proxy video streams, but I’m encountering a problem where longer videos (both online and offline) fail to play correctly. The video starts playing but stops after a few seconds, and the connection seems to drop. I’ve tried both online video URLs and local video files served via a Python server, but the issue persists.
Setup:
• iOS app using Swift and GCDWebServer
• Streaming to Roku device using a custom URL format
• Videos under a certain length (around 2-3 minutes) seem to play fine, but longer videos do not
class CastViewModel: ObservableObject {
@Published var isServerRunning = false
@Published var statusMessage = "Server not running"
private var webServer: GCDWebServer?
private let rokuIP = "192.168.0.XXX" // Hidden IP
private let rokuPort = "XXXX" // Hidden Port
private let logger = Logger(subsystem: "com.yourapp", category: "CastViewModel")
func startServer() {
webServer = GCDWebServer()
// Custom handler using MatchBlock
webServer?.addHandlerWithMatchBlock({ (requestMethod, requestURL, requestHeaders, urlPath, urlQuery) -> GCDWebServerRequest? in
// Only handle GET requests for /video/ path
if requestMethod == "GET", urlPath.hasPrefix("/video/") {
return GCDWebServerRequest(method: requestMethod, url: requestURL, headers: requestHeaders, path: urlPath, query: urlQuery)
}
return nil
}, processBlock: { [weak self] (request) -> GCDWebServerResponse? in
// Processing video request
guard let self = self else { return nil }
let urlString = request.url.absoluteString
guard let range = urlString.range(of: "/video/") else {
return GCDWebServerResponse(statusCode: 400)
}
let videoURL = String(urlString[range.upperBound...])
guard let decodedURL = videoURL.removingPercentEncoding, let validURL = URL(string: decodedURL) else {
self.logger.error("Failed to decode or create a valid URL from: (videoURL)")
return GCDWebServerResponse(statusCode: 400)
}
self.logger.info("Proxying video stream: (decodedURL)")
// Stream the video content from the original URL
URLSession.shared.dataTask(with: validURL) { data, response, error in
guard let data = data, error == nil else {
self.logger.error("Failed to fetch video stream: (error?.localizedDescription ?? "Unknown error")")
return
}
let proxyResponse = GCDWebServerDataResponse(data: data, contentType: "video/mp4")
proxyResponse.statusCode = 200
proxyResponse.setValue("bytes", forAdditionalHeader: "Accept-Ranges")
proxyResponse.cacheControlMaxAge = 360
return proxyResponse
}.resume()
return nil
})
// Start the server
do {
try webServer?.start(options: [
GCDWebServerOption_Port: 8081, // Hidden Port
GCDWebServerOption_BindToLocalhost: false,
GCDWebServerOption_AutomaticallySuspendInBackground: true
])
isServerRunning = true
statusMessage = "Server running on port 8081" // Hidden Port
logger.info("Server started successfully on port 8081") // Hidden Port
} catch {
statusMessage = "Failed to start server: (error.localizedDescription)"
logger.error("Failed to start server: (error.localizedDescription)")
}
}
func castToRoku(with videoURL: String) {
guard let serverIP = getWiFiAddress(), let serverPort = webServer?.port else {
statusMessage = "Failed to get server IP or port"
logger.error("Failed to get server IP or port")
return
}
let serverAddress = "http://(serverIP):(serverPort)" // Hidden IP & Port
// Encode the entire video URL, including the server part
let encodedVideoURL = customURLEncode("(serverAddress)/video/(videoURL)")
// Construct the Roku ECP URL manually
let rokuURLString = "http://(rokuIP):(rokuPort)/input/15985?t=v&u=(encodedVideoURL)&k=(customURLEncode("(serverAddress)/splash/http://d2ucfbcfq1vh3b.cloudfront.net/splash-roku5_free.jpg"))&h=(customURLEncode("(serverAddress)/roku"))&videoName=YouTube_Video&videoFormat=mp4&d=1"
guard let rokuURL = URL(string: rokuURLString) else {
statusMessage = "Failed to construct Roku URL"
logger.error("Failed to construct Roku URL")
return
}
logger.info("Final Roku URL: (rokuURL.absoluteString)")
var request = URLRequest(url: rokuURL)
request.httpMethod = "POST"
// Set headers
request.setValue("*/*", forHTTPHeaderField: "Accept")
request.setValue("gzip, deflate", forHTTPHeaderField: "Accept-Encoding")
request.setValue("en-GB,en;q=0.9", forHTTPHeaderField: "Accept-Language")
request.setValue("no-cache", forHTTPHeaderField: "Cache-Control")
request.setValue("keep-alive", forHTTPHeaderField: "Connection")
request.setValue("text/plain;charset=utf-8", forHTTPHeaderField: "Content-Type")
request.setValue("Cast Browser Roku/3.12.1 CF Network/1496.0.7 Darwin/23.5.0", forHTTPHeaderField: "User-Agent")
logger.info("Sending POST request to Roku")
URLSession.shared.dataTask(with: request) { [weak self] data, response, error in
DispatchQueue.main.async {
if let error = error {
self?.statusMessage = "Error casting to Roku: (error.localizedDescription)"
self?.logger.error("Error casting to Roku: (error.localizedDescription)")
} else if let httpResponse = response as? HTTPURLResponse {
self?.logger.info("Received response from Roku. Status code: (httpResponse.statusCode)")
if httpResponse.statusCode == 200 {
self?.statusMessage = "Successfully cast to Roku"
self?.logger.info("Successfully cast to Roku")
} else {
self?.statusMessage = "Failed to cast to Roku. Status code: (httpResponse.statusCode)"
self?.logger.error("Failed to cast to Roku. Status code: (httpResponse.statusCode)")
if let data = data, let responseBody = String(data: data, encoding: .utf8) {
self?.logger.error("Response body: (responseBody)")
}
}
} else {
self?.statusMessage = "Failed to cast to Roku: Unexpected response"
self?.logger.error("Failed to cast to Roku: Unexpected response")
}
}
}.resume()
}
private func getWiFiAddress() -> String? {
...
}
private func customURLEncode(_ string: String) -> String {
...
}
func sendKeypressToRoku() {
...
}
}
Logs:
[DEBUG] Did open IPv4 listening socket 3
[DEBUG] Did open IPv6 listening socket 4
[INFO] GCDWebServer started on port 8081 and reachable at http://192.168.0.120:8081/
Server started successfully on port 8081
Found WiFi address: 192.168.0.120
Final Roku URL: http://192.168.0.XXX:XXXX/input/15985?t=v&u=http%3A%2F%2F192.168.0.120%3A8081%2Fvideo%2Fhttp%3A%2F%2F192.168.0.XXX%3A8080%2F1.mp4&k=http%3A%2F%2F192.168.0.120%3A8081%2Fsplash%2Fhttp%3A%2F%2Fd2ucfbcfq1vh3b.cloudfront.net%2Fsplash-roku5_free.jpg&h=http%3A%2F%2F192.168.0.120%3A8081%2Froku&videoName=YouTube_Video&videoFormat=mp4&d=1
Sending POST request to Roku
Received response from Roku. Status code: 200
Successfully cast to Roku
[DEBUG] Did open connection on socket 13
[DEBUG] Connection received 138 bytes on socket 13
[DEBUG] Connection on socket 13 processing request “GET /video/http://192.168.0.XXX:8080/1.mp4” with 138 bytes body
Proxying video stream: http://192.168.0.XXX:8080/1.mp4
[DEBUG] Connection sent 845651 bytes on socket 13
[DEBUG] Did close connection on socket 13
[DEBUG] Connection sent 845651 bytes on socket 14
[DEBUG] Did close connection on socket 14
[DEBUG] Connection on socket 13 processing request “GET /roku/state-change/video/play” with 167 bytes body
Proxying video stream: play
[DEBUG] Connection on socket 14 processing request “GET /roku/event/video/started” with 163 bytes body
Proxying video stream: started
Task .<4> finished with error [-1002] Error Domain=NSURLErrorDomain Code=-1002 “unsupported URL” UserInfo={NSLocalizedDescription=unsupported URL, NSErrorFailingURLStringKey=play, NSErrorFailingURLKey=play}
Failed to fetch video stream: unsupported URL
[DEBUG] Connection on socket 13 processing request “GET /roku/event/video/finished” with 164 bytes body
Proxying video stream: finished
Problem:
• The Video of length 5-10 minute plays fine, while greater than that get stuck on loading. even if its a local video.
• Logs indicate that the video data is being sent, but the connection drops abruptly.
• Additionally, I’m receiving errors such as NSURLErrorDomain Code=-1002 "unsupported URL" for certain URLs like "/roku/state-change/video/play".
Questions:
1. Is there something wrong with the way I’m handling video streaming for longer videos?
2. Are there specific configurations for GCDWebServer or URLSession that could prevent this issue?
3. What could be causing the unsupported URL errors for certain non-video URLs?