202 lines
8.0 KiB
Swift
202 lines
8.0 KiB
Swift
import SwiftUI
|
||
import PhotosUI
|
||
import os.log
|
||
import AVKit
|
||
|
||
/// 媒体类型
|
||
public enum MediaType: Equatable {
|
||
case image(UIImage)
|
||
case video(URL, UIImage?) // URL 是视频地址,UIImage 是视频缩略图
|
||
|
||
public var thumbnail: UIImage? {
|
||
switch self {
|
||
case .image(let image):
|
||
return image
|
||
case .video(_, let thumbnail):
|
||
return thumbnail
|
||
}
|
||
}
|
||
|
||
public var isVideo: Bool {
|
||
if case .video = self {
|
||
return true
|
||
}
|
||
return false
|
||
}
|
||
|
||
public static func == (lhs: MediaType, rhs: MediaType) -> Bool {
|
||
switch (lhs, rhs) {
|
||
case (.image(let lhsImage), .image(let rhsImage)):
|
||
return lhsImage.pngData() == rhsImage.pngData()
|
||
case (.video(let lhsURL, _), .video(let rhsURL, _)):
|
||
return lhsURL == rhsURL
|
||
default:
|
||
return false
|
||
}
|
||
}
|
||
}
|
||
|
||
struct MediaPicker: UIViewControllerRepresentable {
|
||
@Binding var selectedMedia: [MediaType]
|
||
let selectionLimit: Int
|
||
let onDismiss: (() -> Void)?
|
||
|
||
func makeUIViewController(context: Context) -> PHPickerViewController {
|
||
var configuration = PHPickerConfiguration(photoLibrary: PHPhotoLibrary.shared())
|
||
configuration.filter = .any(of: [.videos, .images])
|
||
configuration.selectionLimit = selectionLimit
|
||
configuration.preferredAssetRepresentationMode = .current
|
||
|
||
let picker = PHPickerViewController(configuration: configuration)
|
||
picker.delegate = context.coordinator
|
||
return picker
|
||
}
|
||
|
||
func updateUIViewController(_ uiViewController: PHPickerViewController, context: Context) {}
|
||
|
||
func makeCoordinator() -> Coordinator {
|
||
Coordinator(self)
|
||
}
|
||
|
||
class Coordinator: NSObject, PHPickerViewControllerDelegate {
|
||
let parent: MediaPicker
|
||
private let logger = Logger(subsystem: Bundle.main.bundleIdentifier ?? "com.example.app", category: "MediaPicker")
|
||
|
||
init(_ parent: MediaPicker) {
|
||
self.parent = parent
|
||
}
|
||
|
||
func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
|
||
guard !results.isEmpty else {
|
||
parent.onDismiss?()
|
||
return
|
||
}
|
||
|
||
var processedMedia: [MediaType] = []
|
||
let group = DispatchGroup()
|
||
|
||
for result in results {
|
||
let itemProvider = result.itemProvider
|
||
|
||
if itemProvider.hasItemConformingToTypeIdentifier(UTType.image.identifier) {
|
||
group.enter()
|
||
processImage(itemProvider: itemProvider) { media in
|
||
if let media = media {
|
||
processedMedia.append(media)
|
||
}
|
||
group.leave()
|
||
}
|
||
} else if itemProvider.hasItemConformingToTypeIdentifier(UTType.movie.identifier) {
|
||
group.enter()
|
||
processVideo(itemProvider: itemProvider) { media in
|
||
if let media = media {
|
||
processedMedia.append(media)
|
||
}
|
||
group.leave()
|
||
}
|
||
}
|
||
}
|
||
|
||
group.notify(queue: .main) {
|
||
self.parent.selectedMedia = processedMedia
|
||
self.printMediaInfo(media: processedMedia)
|
||
self.parent.onDismiss?()
|
||
}
|
||
}
|
||
|
||
private func processImage(itemProvider: NSItemProvider, completion: @escaping (MediaType?) -> Void) {
|
||
itemProvider.loadObject(ofClass: UIImage.self) { (object, error) in
|
||
if let image = object as? UIImage {
|
||
completion(.image(image))
|
||
} else {
|
||
self.logger.error("Failed to load image: \(error?.localizedDescription ?? "Unknown error")")
|
||
completion(nil)
|
||
}
|
||
}
|
||
}
|
||
|
||
private func processVideo(itemProvider: NSItemProvider, completion: @escaping (MediaType?) -> Void) {
|
||
itemProvider.loadFileRepresentation(forTypeIdentifier: UTType.movie.identifier) { url, error in
|
||
guard let videoURL = url, error == nil else {
|
||
self.logger.error("Failed to load video: \(error?.localizedDescription ?? "Unknown error")")
|
||
completion(nil)
|
||
return
|
||
}
|
||
|
||
let tempDirectory = FileManager.default.temporaryDirectory
|
||
let targetURL = tempDirectory.appendingPathComponent("\(UUID().uuidString).\(videoURL.pathExtension)")
|
||
|
||
do {
|
||
if FileManager.default.fileExists(atPath: targetURL.path) {
|
||
try FileManager.default.removeItem(at: targetURL)
|
||
}
|
||
try FileManager.default.copyItem(at: videoURL, to: targetURL)
|
||
|
||
MediaUtils.extractFirstFrame(from: targetURL) { result in
|
||
switch result {
|
||
case .success(let thumbnail):
|
||
completion(.video(targetURL, thumbnail))
|
||
case .failure(let error):
|
||
self.logger.error("Failed to extract video thumbnail: \(error.localizedDescription)")
|
||
completion(.video(targetURL, nil))
|
||
}
|
||
}
|
||
} catch {
|
||
self.logger.error("Failed to copy video file: \(error.localizedDescription)")
|
||
completion(nil)
|
||
}
|
||
}
|
||
}
|
||
|
||
private func printMediaInfo(media: [MediaType]) {
|
||
print("=== Selected Media Information ===")
|
||
for (index, media) in media.enumerated() {
|
||
print("\nItem \(index + 1):")
|
||
|
||
switch media {
|
||
case .image(let image):
|
||
print("Type: Image")
|
||
print("Dimensions: \(Int(image.size.width))x\(Int(image.size.height))")
|
||
if let data = image.jpegData(compressionQuality: 1.0) {
|
||
print("File Size: \(formatFileSize(Int64(data.count)))")
|
||
}
|
||
|
||
case .video(let url, _):
|
||
print("Type: Video")
|
||
print("File Name: \(url.lastPathComponent)")
|
||
print("File Path: \(url.path)")
|
||
|
||
if let attributes = try? FileManager.default.attributesOfItem(atPath: url.path),
|
||
let fileSize = attributes[.size] as? Int64 {
|
||
print("File Size: \(formatFileSize(fileSize))")
|
||
}
|
||
|
||
let asset = AVURLAsset(url: url)
|
||
let duration = asset.duration.seconds
|
||
print("Duration: \(formatTimeInterval(duration))")
|
||
|
||
if let track = asset.tracks(withMediaType: .video).first {
|
||
let size = track.naturalSize
|
||
print("Video Dimensions: \(Int(size.width))x\(Int(size.height))")
|
||
}
|
||
}
|
||
}
|
||
print("================================\n")
|
||
}
|
||
|
||
private func formatFileSize(_ bytes: Int64) -> String {
|
||
let formatter = ByteCountFormatter()
|
||
formatter.allowedUnits = [.useBytes, .useKB, .useMB, .useGB]
|
||
formatter.countStyle = .file
|
||
return formatter.string(fromByteCount: bytes)
|
||
}
|
||
|
||
private func formatTimeInterval(_ interval: TimeInterval) -> String {
|
||
let formatter = DateComponentsFormatter()
|
||
formatter.allowedUnits = [.hour, .minute, .second]
|
||
formatter.zeroFormattingBehavior = .pad
|
||
return formatter.string(from: interval) ?? "00:00"
|
||
}
|
||
}
|
||
}
|