You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
291 lines
10 KiB
291 lines
10 KiB
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
|
|
}
|
|
}
|