下载地址:https://www.pan38.com/share.php?code=BCjmZ 提取码:8888 【仅供学习参考】
技术原理
通过MobileSubstrate注入系统相机进程
Hook AVFoundation框架的AVCaptureSession相关方法
替换原始视频流数据
核心实现代码
%hook AVCaptureVideoDataOutput
- (void)setSampleBufferDelegate:(id<AVCaptureVideoDataOutputSampleBufferDelegate>)delegate queue:(dispatch_queue_t)queue {
%orig; // 调用原始方法
[VideoFaker shared].realDelegate = delegate; // 保存原始代理
[VideoFaker setupVirtualCamera]; // 初始化虚拟摄像头
}
%end
@interface VideoFaker : NSObject <AVCaptureVideoDataOutputSampleBufferDelegate>
+ (instancetype)shared;
- (void)replaceVideoBuffer:(CMSampleBufferRef)buffer;
@end
代码语言:txt
AI代码解释
VideoFaker {
id<AVCaptureVideoDataOutputSampleBufferDelegate> _realDelegate;
}
- (void)captureOutput:(AVCaptureOutput *)output
didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer
fromConnection:(AVCaptureConnection *)connection {
// 替换为自定义视频帧
CMSampleBufferRef fakeBuffer = [self generateFakeFrame];
[_realDelegate captureOutput:output didOutputSampleBuffer:fakeBuffer fromConnection:connection];
CFRelease(fakeBuffer);
}
- (CMSampleBufferRef)generateFakeFrame {
// 生成虚拟图像逻辑
CVPixelBufferRef pixelBuffer = [FrameGenerator createTestPattern];
return [BufferConverter sampleBufferFromPixelBuffer:pixelBuffer];
}
@end
基于SwiftUI框架设计,包含摄像头切换、滤镜选择和视频源配置功能:
import SwiftUI
import AVKit
struct CameraView: View {
@StateObject private var model = CameraModel()
@State private var showSourcePicker = false
@State private var selectedFilter = "None"
var body: some View {
VStack {
// 视频预览区域
ZStack {
if let frame = model.currentFrame {
Image(uiImage: frame)
.resizable()
.scaledToFit()
.colorMultiply(selectedFilter == "None" ? .white : .accentColor)
} else {
Color.black
}
VStack {
HStack {
Button(action: { model.switchCamera() }) {
Image(systemName: "arrow.triangle.2.circlepath")
.padding(8)
.background(Circle().fill(.ultraThinMaterial))
}
Spacer()
Menu(selectedFilter) {
ForEach(["None", "Sepia", "Mono", "Vintage"], id: \.self) { filter in
Button(filter) {
selectedFilter = filter
model.applyFilter(filter)
}
}
}
}
.padding()
Spacer()
// 视频源选择按钮
Button(action: { showSourcePicker.toggle() }) {
Label("Video Source", systemImage: "film")
.padding()
.background(Capsule().fill(.blue))
}
}
}
// 控制按钮区域
HStack {
Button(action: model.toggleRecording) {
Circle()
.fill(model.isRecording ? .red : .gray)
.frame(width: 60, height: 60)
.overlay(
Circle()
.stroke(.white, lineWidth: 3)
)
}
Spacer()
Button(action: model.capturePhoto) {
Circle()
.fill(.white)
.frame(width: 70, height: 70)
.overlay(
Circle()
.stroke(.blue, lineWidth: 3)
)
}
}
.padding()
}
.sheet(isPresented: $showSourcePicker) {
SourcePickerView(selectedSource: $model.videoSource)
}
}
}
视频选择器(PHPickerViewController)、时长滑块控制(UISlider)和视频裁剪导出功能
import UIKit
import PhotosUI
import AVKit
class VideoPickerViewController: UIViewController {
// MARK: - UI Components
private let selectButton: UIButton = {
let btn = UIButton(type: .system)
btn.setTitle("选择本地视频", for: .normal)
btn.backgroundColor = .systemBlue
btn.tintColor = .white
btn.layer.cornerRadius = 8
return btn
}()
private let timeLabel: UILabel = {
let label = UILabel()
label.text = "选择时长: 0秒"
label.textAlignment = .center
return label
}()
private let slider: UISlider = {
let slider = UISlider()
slider.minimumValue = 0
slider.maximumValue = 60
return slider
}()
private var selectedAsset: AVAsset?
// MARK: - Lifecycle
override func viewDidLoad() {
super.viewDidLoad()
setupUI()
setupActions()
}
// MARK: - Setup
private func setupUI() {
view.backgroundColor = .white
let stack = UIStackView(arrangedSubviews: [selectButton, timeLabel, slider])
stack.axis = .vertical
stack.spacing = 20
stack.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(stack)
NSLayoutConstraint.activate([
stack.centerYAnchor.constraint(equalTo: view.centerYAnchor),
stack.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 40),
stack.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -40)
])
}
private func setupActions() {
selectButton.addTarget(self, action: #selector(selectVideo), for: .touchUpInside)
slider.addTarget(self, action: #selector(sliderValueChanged), for: .valueChanged)
}
// MARK: - Actions
@objc private func selectVideo() {
var config = PHPickerConfiguration()
config.filter = .videos
config.selectionLimit = 1
let picker = PHPickerViewController(configuration: config)
picker.delegate = self
present(picker, animated: true)
}
@objc private func sliderValueChanged() {
let selectedTime = Int(slider.value)
timeLabel.text = "选择时长: \(selectedTime)秒"
}
private func processVideo(with duration: CMTime) {
guard let asset = selectedAsset else { return }
let startTime = CMTime(seconds: 0, preferredTimescale: 600)
let endTime = CMTime(seconds: Double(slider.value), preferredTimescale: 600)
let timeRange = CMTimeRange(start: startTime, end: endTime)
let exportSession = AVAssetExportSession(asset: asset,
presetName: AVAssetExportPresetHighestQuality)
exportSession?.outputURL = FileManager.default.temporaryDirectory
.appendingPathComponent("trimmed_video.mp4")
exportSession?.outputFileType = .mp4
exportSession?.timeRange = timeRange
exportSession?.exportAsynchronously {
DispatchQueue.main.async {
if exportSession?.status == .completed {
self.playVideo(at: exportSession!.outputURL!)
}
}
}
}
private func playVideo(at url: URL) {
let player = AVPlayer(url: url)
let vc = AVPlayerViewController()
vc.player = player
present(vc, animated: true) {
player.play()
}
}
}
// MARK: - PHPickerViewControllerDelegate
extension VideoPickerViewController: PHPickerViewControllerDelegate {
func picker(_ picker: PHPickerViewController,
didFinishPicking results: [PHPickerResult]) {
picker.dismiss(animated: true)
guard let result = results.first else { return }
result.itemProvider.loadObject(ofClass: AVAsset.self) { [weak self] (asset, error) in
guard let self = self, let asset = asset as? AVAsset else { return }
DispatchQueue.main.async {
self.selectedAsset = asset
let duration = asset.duration.seconds
self.slider.maximumValue = Float(duration)
self.timeLabel.text = "选择时长: 0秒/\(Int(duration))秒"
}
}
}
}