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

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
}
}