9 changed files with 50 additions and 524 deletions
@ -1,291 +0,0 @@ |
|||||
import UIKit |
|
||||
import AVFoundation |
|
||||
|
|
||||
/** |
|
||||
* 带内边距的标签 |
|
||||
*/ |
|
||||
class PaddingLabel: UILabel { |
|
||||
var padding: UIEdgeInsets |
|
||||
|
|
||||
init(padding: UIEdgeInsets) { |
|
||||
self.padding = padding |
|
||||
super.init(frame: .zero) |
|
||||
} |
|
||||
|
|
||||
required init?(coder: NSCoder) { |
|
||||
self.padding = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0) |
|
||||
super.init(coder: coder) |
|
||||
} |
|
||||
|
|
||||
override func drawText(in rect: CGRect) { |
|
||||
super.drawText(in: rect.inset(by: padding)) |
|
||||
} |
|
||||
|
|
||||
override var intrinsicContentSize: CGSize { |
|
||||
let size = super.intrinsicContentSize |
|
||||
return CGSize(width: size.width + padding.left + padding.right, |
|
||||
height: size.height + padding.top + padding.bottom) |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* 对话画中画视图 |
|
||||
* |
|
||||
* 用于在画中画模式下显示用户和AI的对话内容 |
|
||||
*/ |
|
||||
@available(iOS 13.0, *) |
|
||||
class DialogPipView: UIView { |
|
||||
|
|
||||
// 视频播放器图层(必须有,否则画中画无法工作) |
|
||||
private var playerLayer: AVPlayerLayer? |
|
||||
private var player: AVPlayer? |
|
||||
|
|
||||
// 对话显示视图 |
|
||||
private let dialogView = UIView() |
|
||||
|
|
||||
// 用户消息标签 |
|
||||
private let userMessageLabel: PaddingLabel = { |
|
||||
let label = PaddingLabel(padding: UIEdgeInsets(top: 8, left: 8, bottom: 8, right: 8)) |
|
||||
label.font = UIFont.systemFont(ofSize: 14) |
|
||||
label.textColor = UIColor.white |
|
||||
label.numberOfLines = 0 |
|
||||
label.backgroundColor = UIColor(red: 0.2, green: 0.2, blue: 0.2, alpha: 0.7) |
|
||||
label.layer.cornerRadius = 8 |
|
||||
label.layer.masksToBounds = true |
|
||||
label.textAlignment = .left |
|
||||
label.isHidden = true |
|
||||
return label |
|
||||
}() |
|
||||
|
|
||||
// AI助手消息标签 |
|
||||
private let assistantMessageLabel: PaddingLabel = { |
|
||||
let label = PaddingLabel(padding: UIEdgeInsets(top: 8, left: 8, bottom: 8, right: 8)) |
|
||||
label.font = UIFont.systemFont(ofSize: 14) |
|
||||
label.textColor = UIColor.white |
|
||||
label.numberOfLines = 0 |
|
||||
label.backgroundColor = UIColor(red: 0.0, green: 0.4, blue: 0.8, alpha: 0.7) |
|
||||
label.layer.cornerRadius = 8 |
|
||||
label.layer.masksToBounds = true |
|
||||
label.textAlignment = .left |
|
||||
label.isHidden = true |
|
||||
return label |
|
||||
}() |
|
||||
|
|
||||
// 初始化 |
|
||||
override init(frame: CGRect) { |
|
||||
super.init(frame: frame) |
|
||||
setupView() |
|
||||
} |
|
||||
|
|
||||
required init?(coder: NSCoder) { |
|
||||
super.init(coder: coder) |
|
||||
setupView() |
|
||||
} |
|
||||
|
|
||||
// 设置视图 |
|
||||
private func setupView() { |
|
||||
backgroundColor = UIColor.black |
|
||||
|
|
||||
// 添加对话视图 |
|
||||
addSubview(dialogView) |
|
||||
dialogView.frame = bounds |
|
||||
dialogView.autoresizingMask = [.flexibleWidth, .flexibleHeight] |
|
||||
|
|
||||
// 添加用户消息标签 |
|
||||
dialogView.addSubview(userMessageLabel) |
|
||||
userMessageLabel.translatesAutoresizingMaskIntoConstraints = false |
|
||||
NSLayoutConstraint.activate([ |
|
||||
userMessageLabel.leadingAnchor.constraint(equalTo: dialogView.leadingAnchor, constant: 8), |
|
||||
userMessageLabel.trailingAnchor.constraint(equalTo: dialogView.trailingAnchor, constant: -8), |
|
||||
userMessageLabel.topAnchor.constraint(equalTo: dialogView.topAnchor, constant: 10), |
|
||||
]) |
|
||||
|
|
||||
// 添加AI助手消息标签 |
|
||||
dialogView.addSubview(assistantMessageLabel) |
|
||||
assistantMessageLabel.translatesAutoresizingMaskIntoConstraints = false |
|
||||
NSLayoutConstraint.activate([ |
|
||||
assistantMessageLabel.leadingAnchor.constraint(equalTo: dialogView.leadingAnchor, constant: 8), |
|
||||
assistantMessageLabel.trailingAnchor.constraint(equalTo: dialogView.trailingAnchor, constant: -8), |
|
||||
assistantMessageLabel.topAnchor.constraint(equalTo: userMessageLabel.bottomAnchor, constant: 10), |
|
||||
assistantMessageLabel.bottomAnchor.constraint(lessThanOrEqualTo: dialogView.bottomAnchor, constant: -10) |
|
||||
]) |
|
||||
} |
|
||||
|
|
||||
// 创建并返回视频图层 |
|
||||
func setupDialogLayer() -> AVPlayerLayer { |
|
||||
// 创建无声视频,用于支持画中画功能 |
|
||||
let playerItem = createSilentVideoPlayerItem() |
|
||||
player = AVPlayer(playerItem: playerItem) |
|
||||
player?.actionAtItemEnd = .none |
|
||||
player?.play() |
|
||||
|
|
||||
// 创建视频图层 |
|
||||
let layer = AVPlayerLayer(player: player) |
|
||||
layer.frame = bounds |
|
||||
layer.videoGravity = .resizeAspectFill |
|
||||
|
|
||||
// 将视频图层放在最底层 |
|
||||
self.layer.insertSublayer(layer, at: 0) |
|
||||
playerLayer = layer |
|
||||
|
|
||||
// 注册通知,确保视频循环播放 |
|
||||
NotificationCenter.default.addObserver(self, |
|
||||
selector: #selector(playerItemDidReachEnd), |
|
||||
name: .AVPlayerItemDidPlayToEndTime, |
|
||||
object: player?.currentItem) |
|
||||
|
|
||||
return layer |
|
||||
} |
|
||||
|
|
||||
// 创建无声视频用于支持画中画 |
|
||||
private func createSilentVideoPlayerItem() -> AVPlayerItem { |
|
||||
// 创建一个简单的黑色视频,足够支持画中画但不会占用太多资源 |
|
||||
let composition = AVMutableComposition() |
|
||||
|
|
||||
// 添加视频轨道 |
|
||||
let videoTrack = composition.addMutableTrack(withMediaType: .video, preferredTrackID: kCMPersistentTrackID_Invalid) |
|
||||
|
|
||||
// 创建一个简单的黑色视频,持续10秒 |
|
||||
let videoSize = CGSize(width: 640, height: 480) |
|
||||
let duration = CMTime(seconds: 10, preferredTimescale: 600) |
|
||||
|
|
||||
// 创建一个黑色图像 |
|
||||
UIGraphicsBeginImageContextWithOptions(videoSize, true, 0) |
|
||||
UIColor.black.setFill() |
|
||||
UIRectFill(CGRect(origin: .zero, size: videoSize)) |
|
||||
let blackImage = UIGraphicsGetImageFromCurrentImageContext() |
|
||||
UIGraphicsEndImageContext() |
|
||||
|
|
||||
// 创建黑色图像的视频 |
|
||||
let settings = [ |
|
||||
AVVideoCodecKey: AVVideoCodecType.h264, |
|
||||
AVVideoWidthKey: videoSize.width, |
|
||||
AVVideoHeightKey: videoSize.height |
|
||||
] as [String : Any] |
|
||||
|
|
||||
let writerInput = AVAssetWriterInput(mediaType: .video, outputSettings: settings) |
|
||||
let adaptor = AVAssetWriterInputPixelBufferAdaptor(assetWriterInput: writerInput, sourcePixelBufferAttributes: nil) |
|
||||
|
|
||||
if let pixelBuffer = createPixelBuffer(fromImage: blackImage!, size: videoSize) { |
|
||||
let attributes: [String: Any] = [ |
|
||||
kCVPixelBufferPixelFormatTypeKey as String: kCVPixelFormatType_32ARGB, |
|
||||
kCVPixelBufferWidthKey as String: videoSize.width, |
|
||||
kCVPixelBufferHeightKey as String: videoSize.height |
|
||||
] |
|
||||
|
|
||||
// 创建一个5秒的黑色视频 |
|
||||
let assetWriter = try! AVAssetWriter(outputURL: URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent("temp.mov"), fileType: .mov) |
|
||||
assetWriter.add(writerInput) |
|
||||
assetWriter.startWriting() |
|
||||
assetWriter.startSession(atSourceTime: .zero) |
|
||||
|
|
||||
let frameCount = 50 // 5秒,10fps |
|
||||
let frameDuration = CMTime(value: 1, timescale: 10) |
|
||||
|
|
||||
writerInput.requestMediaDataWhenReady(on: DispatchQueue.global()) { |
|
||||
for i in 0..<frameCount { |
|
||||
while !writerInput.isReadyForMoreMediaData {} |
|
||||
|
|
||||
let presentationTime = CMTime(value: CMTimeValue(i), timescale: 10) |
|
||||
adaptor.append(pixelBuffer, withPresentationTime: presentationTime) |
|
||||
} |
|
||||
|
|
||||
writerInput.markAsFinished() |
|
||||
assetWriter.finishWriting {} |
|
||||
} |
|
||||
|
|
||||
// 等待视频写入完成 |
|
||||
while assetWriter.status == .writing {} |
|
||||
|
|
||||
// 创建AVPlayerItem |
|
||||
let asset = AVAsset(url: assetWriter.outputURL) |
|
||||
return AVPlayerItem(asset: asset) |
|
||||
} |
|
||||
|
|
||||
// 如果上面的方法失败,创建一个简单的默认项 |
|
||||
let emptyItem = AVPlayerItem(url: URL(fileURLWithPath: "")) |
|
||||
return emptyItem |
|
||||
} |
|
||||
|
|
||||
// 从UIImage创建像素缓冲区 |
|
||||
private func createPixelBuffer(fromImage image: UIImage, size: CGSize) -> CVPixelBuffer? { |
|
||||
var pixelBuffer: CVPixelBuffer? |
|
||||
let attrs = [ |
|
||||
kCVPixelBufferCGImageCompatibilityKey: kCFBooleanTrue, |
|
||||
kCVPixelBufferCGBitmapContextCompatibilityKey: kCFBooleanTrue |
|
||||
] as CFDictionary |
|
||||
|
|
||||
let status = CVPixelBufferCreate( |
|
||||
kCFAllocatorDefault, |
|
||||
Int(size.width), |
|
||||
Int(size.height), |
|
||||
kCVPixelFormatType_32ARGB, |
|
||||
attrs, |
|
||||
&pixelBuffer |
|
||||
) |
|
||||
|
|
||||
guard status == kCVReturnSuccess, let buffer = pixelBuffer else { |
|
||||
return nil |
|
||||
} |
|
||||
|
|
||||
CVPixelBufferLockBaseAddress(buffer, []) |
|
||||
let context = CGContext( |
|
||||
data: CVPixelBufferGetBaseAddress(buffer), |
|
||||
width: Int(size.width), |
|
||||
height: Int(size.height), |
|
||||
bitsPerComponent: 8, |
|
||||
bytesPerRow: CVPixelBufferGetBytesPerRow(buffer), |
|
||||
space: CGColorSpaceCreateDeviceRGB(), |
|
||||
bitmapInfo: CGImageAlphaInfo.noneSkipFirst.rawValue |
|
||||
) |
|
||||
|
|
||||
context?.draw(image.cgImage!, in: CGRect(x: 0, y: 0, width: size.width, height: size.height)) |
|
||||
CVPixelBufferUnlockBaseAddress(buffer, []) |
|
||||
|
|
||||
return buffer |
|
||||
} |
|
||||
|
|
||||
// 视频播放结束处理 |
|
||||
@objc private func playerItemDidReachEnd(notification: Notification) { |
|
||||
// 循环播放视频 |
|
||||
player?.seek(to: .zero) |
|
||||
player?.play() |
|
||||
} |
|
||||
|
|
||||
// 更新对话内容 |
|
||||
func updateDialogContent(userMessage: String, assistantMessage: String) { |
|
||||
// 设置用户消息 |
|
||||
if !userMessage.isEmpty { |
|
||||
userMessageLabel.text = "用户: \(userMessage)" |
|
||||
userMessageLabel.isHidden = false |
|
||||
} else { |
|
||||
userMessageLabel.isHidden = true |
|
||||
} |
|
||||
|
|
||||
// 设置AI助手消息 |
|
||||
if !assistantMessage.isEmpty { |
|
||||
assistantMessageLabel.text = "AI助手: \(assistantMessage)" |
|
||||
assistantMessageLabel.isHidden = false |
|
||||
} else { |
|
||||
assistantMessageLabel.isHidden = true |
|
||||
} |
|
||||
|
|
||||
// 更新布局 |
|
||||
setNeedsLayout() |
|
||||
} |
|
||||
|
|
||||
// 布局子视图 |
|
||||
override func layoutSubviews() { |
|
||||
super.layoutSubviews() |
|
||||
|
|
||||
// 调整视频图层大小 |
|
||||
playerLayer?.frame = bounds |
|
||||
} |
|
||||
|
|
||||
// 释放资源 |
|
||||
deinit { |
|
||||
NotificationCenter.default.removeObserver(self) |
|
||||
player?.pause() |
|
||||
player = nil |
|
||||
} |
|
||||
} |
|
||||
Loading…
Reference in new issue