diff --git a/Sora/Assets.xcassets/AppIcon.appiconset/darkmode.png b/Sora/Assets.xcassets/AppIcon.appiconset/darkmode.png index 4048369..c8f6f6f 100644 Binary files a/Sora/Assets.xcassets/AppIcon.appiconset/darkmode.png and b/Sora/Assets.xcassets/AppIcon.appiconset/darkmode.png differ diff --git a/Sora/Assets.xcassets/AppIcon.appiconset/lightmode.png b/Sora/Assets.xcassets/AppIcon.appiconset/lightmode.png index 4048369..7256d08 100644 Binary files a/Sora/Assets.xcassets/AppIcon.appiconset/lightmode.png and b/Sora/Assets.xcassets/AppIcon.appiconset/lightmode.png differ diff --git a/Sora/Assets.xcassets/AppIcon.appiconset/tinting.png b/Sora/Assets.xcassets/AppIcon.appiconset/tinting.png index 4048369..f55a3d9 100644 Binary files a/Sora/Assets.xcassets/AppIcon.appiconset/tinting.png and b/Sora/Assets.xcassets/AppIcon.appiconset/tinting.png differ diff --git a/Sora/MediaUtils/CustomPlayer/Helpers/VTTSubtitlesLoader.swift b/Sora/MediaUtils/CustomPlayer/Helpers/VTTSubtitlesLoader.swift index 1f23c23..d3d2682 100644 --- a/Sora/MediaUtils/CustomPlayer/Helpers/VTTSubtitlesLoader.swift +++ b/Sora/MediaUtils/CustomPlayer/Helpers/VTTSubtitlesLoader.swift @@ -5,7 +5,6 @@ // Created by Francesco on 15/02/25. // -import Combine import Foundation struct SubtitleCue: Identifiable { @@ -27,24 +26,30 @@ class VTTSubtitlesLoader: ObservableObject { func load(from urlString: String) { guard let url = URL(string: urlString) else { return } - let format = determineSubtitleFormat(from: url) - - URLSession.shared.dataTask(with: url) { data, _, error in - guard let data = data, - let content = String(data: data, encoding: .utf8), - error == nil else { return } + URLSession.shared.dataTask(with: url) { data, response, error in + guard let responseData = data, + let subtitleContent = String(data: responseData, encoding: .utf8), + !subtitleContent.isEmpty, + error == nil else { + DispatchQueue.main.async { + self.cues = [] + } + return + } + + let detectedFormat = self.determineSubtitleFormat(from: url) DispatchQueue.main.async { - switch format { + switch detectedFormat { case .vtt: - self.cues = self.parseVTT(content: content) + self.cues = self.parseVTT(content: subtitleContent) case .srt: - self.cues = self.parseSRT(content: content) + self.cues = self.parseSRT(content: subtitleContent) case .unknown: - if content.trimmed.hasPrefix("WEBVTT") { - self.cues = self.parseVTT(content: content) + if subtitleContent.trimmingCharacters(in: .whitespacesAndNewlines).hasPrefix("WEBVTT") { + self.cues = self.parseVTT(content: subtitleContent) } else { - self.cues = self.parseSRT(content: content) + self.cues = self.parseSRT(content: subtitleContent) } } } @@ -64,46 +69,70 @@ class VTTSubtitlesLoader: ObservableObject { } private func parseVTT(content: String) -> [SubtitleCue] { - var cues: [SubtitleCue] = [] - let lines = content.components(separatedBy: .newlines) - var index = 0 + let contentLines = content.components(separatedBy: .newlines) + var subtitleCues: [SubtitleCue] = [] + var activeStartTime: Double? + var activeEndTime: Double? + var activeSubtitleText: String = "" + var processingCueContent = false - while index < lines.count { - let line = lines[index].trimmingCharacters(in: .whitespaces) - if line.isEmpty || line == "WEBVTT" { - index += 1 + for (lineIndex, currentLine) in contentLines.enumerated() { + let cleanedLine = currentLine.trimmingCharacters(in: .whitespacesAndNewlines) + + if cleanedLine.isEmpty || cleanedLine == "WEBVTT" || cleanedLine.starts(with: "NOTE") { + if processingCueContent && !activeSubtitleText.isEmpty { + if let beginTime = activeStartTime, let finishTime = activeEndTime { + subtitleCues.append(SubtitleCue(startTime: beginTime, endTime: finishTime, text: activeSubtitleText)) + } + processingCueContent = false + activeStartTime = nil + activeEndTime = nil + activeSubtitleText = "" + } continue } - if !line.contains("-->") { - index += 1 - if index >= lines.count { break } + if cleanedLine.contains("-->") { + if processingCueContent && !activeSubtitleText.isEmpty { + if let beginTime = activeStartTime, let finishTime = activeEndTime { + subtitleCues.append(SubtitleCue(startTime: beginTime, endTime: finishTime, text: activeSubtitleText)) + } + } + + let timeComponents = cleanedLine.components(separatedBy: "-->").map { $0.trimmingCharacters(in: .whitespaces) } + if timeComponents.count == 2 { + activeStartTime = parseVTTTime(timeComponents[0]) + activeEndTime = parseVTTTime(timeComponents[1]) + activeSubtitleText = "" + processingCueContent = true + } + } else if processingCueContent { + if !activeSubtitleText.isEmpty { + activeSubtitleText += "\n" + } + activeSubtitleText += cleanedLine + + let isFinalLine = lineIndex == contentLines.count - 1 + let followingLine = isFinalLine ? "" : contentLines[lineIndex + 1].trimmingCharacters(in: .whitespacesAndNewlines) + if isFinalLine || followingLine.isEmpty || followingLine.contains("-->") { + if let beginTime = activeStartTime, let finishTime = activeEndTime { + subtitleCues.append(SubtitleCue(startTime: beginTime, endTime: finishTime, text: activeSubtitleText)) + activeStartTime = nil + activeEndTime = nil + activeSubtitleText = "" + processingCueContent = false + } + } } - - let timeLine = lines[index] - let times = timeLine.components(separatedBy: "-->") - if times.count < 2 { - index += 1 - continue - } - - let startTime = parseTimecode(times[0].trimmingCharacters(in: .whitespaces)) - let endTime = parseTimecode(times[1].trimmingCharacters(in: .whitespaces)) - index += 1 - var cueText = "" - while index < lines.count && !lines[index].trimmingCharacters(in: .whitespaces).isEmpty { - cueText += lines[index] + "\n" - index += 1 - } - cues.append(SubtitleCue(startTime: startTime, endTime: endTime, text: cueText.trimmingCharacters(in: .whitespacesAndNewlines))) } - return cues + + return subtitleCues } private func parseSRT(content: String) -> [SubtitleCue] { var cues: [SubtitleCue] = [] let normalizedContent = content.replacingOccurrences(of: "\r\n", with: "\n") - .replacingOccurrences(of: "\r", with: "\n") + .replacingOccurrences(of: "\r", with: "\n") let blocks = normalizedContent.components(separatedBy: "\n\n") for block in blocks { @@ -130,6 +159,34 @@ class VTTSubtitlesLoader: ObservableObject { return cues } + private func parseVTTTime(_ timeString: String) -> Double { + let timeSegments = timeString.components(separatedBy: ":") + guard timeSegments.count >= 2 else { return 0 } + + var hourValue = 0.0 + var minuteValue = 0.0 + var secondValue = 0.0 + + if timeSegments.count == 3 { + hourValue = Double(timeSegments[0]) ?? 0 + minuteValue = Double(timeSegments[1]) ?? 0 + let secondComponents = timeSegments[2].components(separatedBy: ".") + secondValue = Double(secondComponents[0]) ?? 0 + if secondComponents.count > 1 { + secondValue += Double("0." + secondComponents[1]) ?? 0 + } + } else { + minuteValue = Double(timeSegments[0]) ?? 0 + let secondComponents = timeSegments[1].components(separatedBy: ".") + secondValue = Double(secondComponents[0]) ?? 0 + if secondComponents.count > 1 { + secondValue += Double("0." + secondComponents[1]) ?? 0 + } + } + + return hourValue * 3600 + minuteValue * 60 + secondValue + } + private func parseTimecode(_ timeString: String) -> Double { let parts = timeString.components(separatedBy: ":") var seconds = 0.0