mirror of
https://github.com/yattee/yattee.git
synced 2026-02-20 01:39:46 +00:00
Yattee v2 rewrite
This commit is contained in:
270
Yattee/Models/Stream.swift
Normal file
270
Yattee/Models/Stream.swift
Normal file
@@ -0,0 +1,270 @@
|
||||
//
|
||||
// Stream.swift
|
||||
// Yattee
|
||||
//
|
||||
// Represents a video/audio stream for playback.
|
||||
//
|
||||
|
||||
@preconcurrency import Foundation
|
||||
|
||||
/// Represents a playable video or audio stream.
|
||||
struct Stream: Identifiable, Codable, Hashable, Sendable {
|
||||
/// Unique identifier combining resolution, fps and format.
|
||||
var id: String {
|
||||
let fpsString = fps.map { "\($0)" } ?? ""
|
||||
return "\(resolution?.description ?? "audio")\(fpsString)-\(format)"
|
||||
}
|
||||
|
||||
/// The stream URL.
|
||||
let url: URL
|
||||
|
||||
/// Stream resolution (nil for audio-only).
|
||||
let resolution: StreamResolution?
|
||||
|
||||
/// Format/codec (mp4, webm, etc.).
|
||||
let format: String
|
||||
|
||||
/// Video codec if applicable.
|
||||
let videoCodec: String?
|
||||
|
||||
/// Audio codec.
|
||||
let audioCodec: String?
|
||||
|
||||
/// Bitrate in bits per second.
|
||||
let bitrate: Int?
|
||||
|
||||
/// File size in bytes if known.
|
||||
let fileSize: Int64?
|
||||
|
||||
/// Whether this is an audio-only stream.
|
||||
let isAudioOnly: Bool
|
||||
|
||||
/// Whether this is a video-only stream (no audio track).
|
||||
/// Note: Some sites (e.g., BitChute) don't provide resolution info.
|
||||
var isVideoOnly: Bool {
|
||||
// HLS/DASH are adaptive formats - codec info is in manifest, not metadata
|
||||
// They should never be classified as video-only based on missing codec info
|
||||
let isAdaptive = format.lowercased().contains("hls") ||
|
||||
format.lowercased().contains("dash") ||
|
||||
format.lowercased().contains("m3u8") ||
|
||||
format.lowercased().contains("mpd")
|
||||
guard !isAdaptive else { return false }
|
||||
return !isAudioOnly && audioCodec == nil
|
||||
}
|
||||
|
||||
/// Whether this stream has both video and audio (muxed).
|
||||
/// Note: Some sites (e.g., BitChute) don't provide resolution info.
|
||||
var isMuxed: Bool {
|
||||
!isAudioOnly && audioCodec != nil
|
||||
}
|
||||
|
||||
/// Whether this is a live stream (HLS/DASH).
|
||||
let isLive: Bool
|
||||
|
||||
/// MIME type.
|
||||
let mimeType: String?
|
||||
|
||||
/// Audio language code (e.g., "en", "es", "ja").
|
||||
let audioLanguage: String?
|
||||
|
||||
/// Audio track name/label.
|
||||
let audioTrackName: String?
|
||||
|
||||
/// Whether this is the original audio track (not dubbed).
|
||||
let isOriginalAudio: Bool
|
||||
|
||||
/// Custom HTTP headers required for streaming (cookies, referer, etc.).
|
||||
let httpHeaders: [String: String]?
|
||||
|
||||
/// Frame rate (fps) if known.
|
||||
let fps: Int?
|
||||
|
||||
// MARK: - Initialization
|
||||
|
||||
init(
|
||||
url: URL,
|
||||
resolution: StreamResolution? = nil,
|
||||
format: String,
|
||||
videoCodec: String? = nil,
|
||||
audioCodec: String? = nil,
|
||||
bitrate: Int? = nil,
|
||||
fileSize: Int64? = nil,
|
||||
isAudioOnly: Bool = false,
|
||||
isLive: Bool = false,
|
||||
mimeType: String? = nil,
|
||||
audioLanguage: String? = nil,
|
||||
audioTrackName: String? = nil,
|
||||
isOriginalAudio: Bool = false,
|
||||
httpHeaders: [String: String]? = nil,
|
||||
fps: Int? = nil
|
||||
) {
|
||||
self.url = url
|
||||
self.resolution = resolution
|
||||
self.format = format
|
||||
self.videoCodec = videoCodec
|
||||
self.audioCodec = audioCodec
|
||||
self.bitrate = bitrate
|
||||
self.fileSize = fileSize
|
||||
self.isAudioOnly = isAudioOnly
|
||||
self.isLive = isLive
|
||||
self.mimeType = mimeType
|
||||
self.audioLanguage = audioLanguage
|
||||
self.audioTrackName = audioTrackName
|
||||
self.isOriginalAudio = isOriginalAudio
|
||||
self.httpHeaders = httpHeaders
|
||||
self.fps = fps
|
||||
}
|
||||
|
||||
// MARK: - Computed Properties
|
||||
|
||||
var qualityLabel: String {
|
||||
if isAudioOnly {
|
||||
return "Audio"
|
||||
}
|
||||
guard let resolution else { return "Unknown" }
|
||||
if let fps {
|
||||
return "\(resolution.height)p · \(fps)fps"
|
||||
}
|
||||
return resolution.description
|
||||
}
|
||||
|
||||
var formattedFileSize: String? {
|
||||
guard let fileSize else { return nil }
|
||||
return ByteCountFormatter.string(fromByteCount: fileSize, countStyle: .file)
|
||||
}
|
||||
|
||||
/// Whether this stream is likely playable on Apple platforms.
|
||||
var isNativelyPlayable: Bool {
|
||||
// Apple platforms prefer H.264/H.265 in MP4/MOV containers
|
||||
let supportedFormats = ["mp4", "mov", "m4v", "m4a"]
|
||||
let supportedVideoCodecs = ["avc1", "hvc1", "hev1", "h264", "hevc"]
|
||||
|
||||
if isAudioOnly {
|
||||
return true // Most audio codecs are supported
|
||||
}
|
||||
|
||||
let formatSupported = supportedFormats.contains { format.lowercased().contains($0) }
|
||||
let codecSupported = videoCodec.map { codec in
|
||||
supportedVideoCodecs.contains { codec.lowercased().contains($0) }
|
||||
} ?? true
|
||||
|
||||
return formatSupported && codecSupported
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Stream Resolution
|
||||
|
||||
struct StreamResolution: Codable, Hashable, Sendable, Comparable, CustomStringConvertible {
|
||||
let width: Int
|
||||
let height: Int
|
||||
|
||||
var description: String {
|
||||
"\(height)p"
|
||||
}
|
||||
|
||||
// Common resolutions
|
||||
static let p360 = StreamResolution(width: 640, height: 360)
|
||||
static let p480 = StreamResolution(width: 854, height: 480)
|
||||
static let p720 = StreamResolution(width: 1280, height: 720)
|
||||
static let p1080 = StreamResolution(width: 1920, height: 1080)
|
||||
static let p1440 = StreamResolution(width: 2560, height: 1440)
|
||||
static let p2160 = StreamResolution(width: 3840, height: 2160)
|
||||
|
||||
static func < (lhs: StreamResolution, rhs: StreamResolution) -> Bool {
|
||||
lhs.height < rhs.height
|
||||
}
|
||||
|
||||
init(width: Int, height: Int) {
|
||||
self.width = width
|
||||
self.height = height
|
||||
}
|
||||
|
||||
init?(heightLabel: String) {
|
||||
let cleaned = heightLabel.replacingOccurrences(of: "p", with: "")
|
||||
guard let height = Int(cleaned) else { return nil }
|
||||
|
||||
// Estimate width from height assuming 16:9
|
||||
let width = (height * 16) / 9
|
||||
self.init(width: width, height: height)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Preview Data
|
||||
|
||||
extension Stream {
|
||||
/// A sample 1080p muxed stream for SwiftUI previews.
|
||||
static var preview: Stream {
|
||||
Stream(
|
||||
url: URL(string: "https://example.com/video.mp4")!,
|
||||
resolution: .p1080,
|
||||
format: "mp4",
|
||||
videoCodec: "avc1",
|
||||
audioCodec: "mp4a",
|
||||
bitrate: 5_000_000,
|
||||
fileSize: 150_000_000,
|
||||
isAudioOnly: false,
|
||||
isLive: false,
|
||||
mimeType: "video/mp4",
|
||||
fps: 30
|
||||
)
|
||||
}
|
||||
|
||||
/// A sample video-only stream (VP9) for SwiftUI previews.
|
||||
static var videoOnlyPreview: Stream {
|
||||
Stream(
|
||||
url: URL(string: "https://example.com/video-only.webm")!,
|
||||
resolution: .p1080,
|
||||
format: "webm",
|
||||
videoCodec: "vp9",
|
||||
audioCodec: nil,
|
||||
bitrate: 3_000_000,
|
||||
fileSize: 100_000_000,
|
||||
isAudioOnly: false,
|
||||
isLive: false,
|
||||
mimeType: "video/webm",
|
||||
fps: 60
|
||||
)
|
||||
}
|
||||
|
||||
/// A sample audio-only stream for SwiftUI previews.
|
||||
static var audioPreview: Stream {
|
||||
Stream(
|
||||
url: URL(string: "https://example.com/audio.m4a")!,
|
||||
resolution: nil,
|
||||
format: "m4a",
|
||||
videoCodec: nil,
|
||||
audioCodec: "opus",
|
||||
bitrate: 128_000,
|
||||
fileSize: 5_000_000,
|
||||
isAudioOnly: true,
|
||||
isLive: false,
|
||||
mimeType: "audio/mp4",
|
||||
audioLanguage: "en",
|
||||
audioTrackName: "English",
|
||||
isOriginalAudio: true
|
||||
)
|
||||
}
|
||||
|
||||
/// A sample HLS adaptive stream for SwiftUI previews.
|
||||
static var hlsPreview: Stream {
|
||||
Stream(
|
||||
url: URL(string: "https://example.com/master.m3u8")!,
|
||||
resolution: .p1080,
|
||||
format: "hls",
|
||||
isLive: false,
|
||||
mimeType: "application/vnd.apple.mpegurl",
|
||||
fps: 60
|
||||
)
|
||||
}
|
||||
|
||||
/// A sample HLS stream without quality info for SwiftUI previews.
|
||||
static var hlsNoQualityPreview: Stream {
|
||||
Stream(
|
||||
url: URL(string: "https://example.com/master2.m3u8")!,
|
||||
resolution: nil,
|
||||
format: "hls",
|
||||
isLive: false,
|
||||
mimeType: "application/vnd.apple.mpegurl"
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user