feat: 视频水印固定位置显示;允许截屏禁止录屏
This commit is contained in:
@@ -12,6 +12,16 @@ class CaptureScreenTool {
|
||||
static listener : UserCaptureScreenCallback | null;
|
||||
static secureView : UIView | null;
|
||||
|
||||
// ===== 仅防录屏(动态切换)相关状态 =====
|
||||
// 保留 UITextField,使 isSecureTextEntry 可在录屏开始/结束时反复切换
|
||||
static secureField : UITextField | null;
|
||||
// field 的安全画布(已挂入视图层级,承载 App 根视图)
|
||||
static secureCanvas : UIView | null;
|
||||
// 被安全画布包裹的 App 根视图(用于卸载时还原)
|
||||
static secureRootView : UIView | null;
|
||||
// 是否已为"仅防录屏"挂载安全层
|
||||
static recordWrapped : boolean = false;
|
||||
|
||||
// 监听截屏
|
||||
static listenCaptureScreen(callback : UserCaptureScreenCallback | null) {
|
||||
this.listener = callback
|
||||
@@ -51,28 +61,38 @@ class CaptureScreenTool {
|
||||
return null
|
||||
}
|
||||
|
||||
// 应用防截屏遮罩(secureView:实时显示正常,但被截屏/录屏捕获到的内容为黑屏)
|
||||
// 不触发回调,供内部复用;注意:需在主线程调用
|
||||
static applySecureViewInternal() {
|
||||
if (this.secureView != null) {
|
||||
// 已应用,避免重复包裹
|
||||
return
|
||||
}
|
||||
let secureView = this.createSecureView()
|
||||
let window = UTSiOS.getKeyWindow()
|
||||
let rootView = window.rootViewController == null ? null : window.rootViewController!.view
|
||||
if (secureView != null && rootView != null) {
|
||||
let rootSuperview = rootView!.superview
|
||||
if (rootSuperview != null) {
|
||||
this.secureView = secureView
|
||||
rootSuperview!.addSubview(secureView!)
|
||||
rootView!.removeFromSuperview()
|
||||
secureView!.addSubview(rootView!)
|
||||
// secureView 充满父视图并随父视图宽高自适应(横竖屏、全屏切换时自动跟随)
|
||||
secureView!.frame = rootSuperview!.bounds
|
||||
secureView!.autoresizingMask = [UIView.AutoresizingMask.flexibleWidth, UIView.AutoresizingMask.flexibleHeight]
|
||||
// rootView 充满 secureView 并自适应,避免全屏时被固定在竖屏尺寸而缩到角落
|
||||
rootView!.frame = secureView!.bounds
|
||||
rootView!.autoresizingMask = [UIView.AutoresizingMask.flexibleWidth, UIView.AutoresizingMask.flexibleHeight]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 开启防截屏
|
||||
static onAntiScreenshot(option : SetUserCaptureScreenOptions) {
|
||||
// uts方法默认会在子线程中执行,涉及 UI 操作必须在主线程中运行,通过 DispatchQueue.main.async 方法可将代码在主线程中运行
|
||||
DispatchQueue.main.async(execute = () : void => {
|
||||
let secureView = this.createSecureView()
|
||||
let window = UTSiOS.getKeyWindow()
|
||||
let rootView = window.rootViewController == null ? null : window.rootViewController!.view
|
||||
if (secureView != null && rootView != null) {
|
||||
let rootSuperview = rootView!.superview
|
||||
if (rootSuperview != null) {
|
||||
this.secureView = secureView
|
||||
rootSuperview!.addSubview(secureView!)
|
||||
rootView!.removeFromSuperview()
|
||||
secureView!.addSubview(rootView!)
|
||||
// secureView 充满父视图并随父视图宽高自适应(横竖屏、全屏切换时自动跟随)
|
||||
secureView!.frame = rootSuperview!.bounds
|
||||
secureView!.autoresizingMask = [UIView.AutoresizingMask.flexibleWidth, UIView.AutoresizingMask.flexibleHeight]
|
||||
// rootView 充满 secureView 并自适应,避免全屏时被固定在竖屏尺寸而缩到角落
|
||||
rootView!.frame = secureView!.bounds
|
||||
rootView!.autoresizingMask = [UIView.AutoresizingMask.flexibleWidth, UIView.AutoresizingMask.flexibleHeight]
|
||||
}
|
||||
}
|
||||
this.applySecureViewInternal()
|
||||
let res: SetUserCaptureScreenSuccess = {
|
||||
}
|
||||
option.success?.(res)
|
||||
@@ -80,21 +100,107 @@ class CaptureScreenTool {
|
||||
})
|
||||
}
|
||||
|
||||
// ===== 仅防录屏:提前挂载安全层 + 翻转 isSecureTextEntry =====
|
||||
|
||||
// 提前挂载安全层(应在录屏开始前调用,例如进入页面时)。
|
||||
// 挂载后默认不保护(isSecureTextEntry = false)→ 截屏正常、实时正常。
|
||||
// 注意:需在主线程调用。
|
||||
static setupRecordSecureWrapper() {
|
||||
if (this.recordWrapped) {
|
||||
return
|
||||
}
|
||||
let field = new UITextField(frame = CGRect.zero)
|
||||
field.isSecureTextEntry = true
|
||||
// 15.1 系统 secureView 不可用;拿不到安全画布则放弃
|
||||
if (field.subviews.length == 0 || UIDevice.current.systemVersion == '15.1') {
|
||||
return
|
||||
}
|
||||
let canvas = field.subviews[0]
|
||||
canvas.subviews.forEach((item) => {
|
||||
item.removeFromSuperview()
|
||||
})
|
||||
canvas.isUserInteractionEnabled = true
|
||||
|
||||
let window = UTSiOS.getKeyWindow()
|
||||
let rootView = window.rootViewController == null ? null : window.rootViewController!.view
|
||||
if (rootView == null) {
|
||||
return
|
||||
}
|
||||
let rootSuperview = rootView!.superview
|
||||
if (rootSuperview == null) {
|
||||
return
|
||||
}
|
||||
|
||||
// 把安全画布插入原层级,并把 App 根视图移入画布
|
||||
rootSuperview!.addSubview(canvas)
|
||||
rootView!.removeFromSuperview()
|
||||
canvas.addSubview(rootView!)
|
||||
canvas.frame = rootSuperview!.bounds
|
||||
canvas.autoresizingMask = [UIView.AutoresizingMask.flexibleWidth, UIView.AutoresizingMask.flexibleHeight]
|
||||
rootView!.frame = canvas.bounds
|
||||
rootView!.autoresizingMask = [UIView.AutoresizingMask.flexibleWidth, UIView.AutoresizingMask.flexibleHeight]
|
||||
|
||||
// 关键:保留 field,后续才能切换 isSecureTextEntry
|
||||
this.secureField = field
|
||||
this.secureCanvas = canvas
|
||||
this.secureRootView = rootView
|
||||
this.recordWrapped = true
|
||||
|
||||
// 默认不保护:允许截屏、实时正常
|
||||
field.isSecureTextEntry = false
|
||||
}
|
||||
|
||||
// 切换录屏保护:true=保护(录屏文件黑、实时正常、此刻截屏也黑);false=不保护(允许截屏)
|
||||
// 注意:需在主线程调用。
|
||||
static setRecordSecure(secure : boolean) {
|
||||
if (this.secureField != null) {
|
||||
this.secureField!.isSecureTextEntry = secure
|
||||
}
|
||||
}
|
||||
|
||||
// 卸载安全层,恢复原始视图层级。注意:需在主线程调用。
|
||||
static teardownRecordSecureWrapper() {
|
||||
if (!this.recordWrapped) {
|
||||
return
|
||||
}
|
||||
if (this.secureField != null) {
|
||||
this.secureField!.isSecureTextEntry = false
|
||||
}
|
||||
if (this.secureCanvas != null && this.secureRootView != null) {
|
||||
let rootSuperview = this.secureCanvas!.superview
|
||||
if (rootSuperview != null) {
|
||||
rootSuperview!.addSubview(this.secureRootView!)
|
||||
this.secureRootView!.frame = rootSuperview!.bounds
|
||||
this.secureCanvas!.removeFromSuperview()
|
||||
}
|
||||
}
|
||||
this.secureField = null
|
||||
this.secureCanvas = null
|
||||
this.secureRootView = null
|
||||
this.recordWrapped = false
|
||||
}
|
||||
|
||||
// 移除防截屏遮罩视图(不触发回调,供内部复用)
|
||||
// 注意:需在主线程调用
|
||||
static removeSecureViewInternal() {
|
||||
if (this.secureView != null) {
|
||||
let window = UTSiOS.getKeyWindow()
|
||||
let rootView = window.rootViewController == null ? null : window.rootViewController!.view
|
||||
if (rootView != null && this.secureView!.superview != null) {
|
||||
let rootSuperview = this.secureView!.superview
|
||||
if (rootSuperview != null) {
|
||||
rootSuperview!.addSubview(rootView!)
|
||||
this.secureView!.removeFromSuperview()
|
||||
}
|
||||
}
|
||||
this.secureView = null
|
||||
}
|
||||
}
|
||||
|
||||
// 关闭防截屏
|
||||
static offAntiScreenshot(option : SetUserCaptureScreenOptions) {
|
||||
DispatchQueue.main.async(execute = () : void => {
|
||||
if (this.secureView != null) {
|
||||
let window = UTSiOS.getKeyWindow()
|
||||
let rootView = window.rootViewController == null ? null : window.rootViewController!.view
|
||||
if (rootView != null && this.secureView!.superview != null) {
|
||||
let rootSuperview = this.secureView!.superview
|
||||
if (rootSuperview != null) {
|
||||
rootSuperview!.addSubview(rootView!)
|
||||
this.secureView!.removeFromSuperview()
|
||||
}
|
||||
}
|
||||
this.secureView = null
|
||||
}
|
||||
this.removeSecureViewInternal()
|
||||
let res: SetUserCaptureScreenSuccess = {
|
||||
}
|
||||
option.success?.(res)
|
||||
@@ -103,6 +209,58 @@ class CaptureScreenTool {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 防录屏工具类:基于 UIScreen.isCaptured 监听录屏/投屏状态,动态切换安全层。
|
||||
* 截屏不会触发 isCaptured,因此可做到"允许截屏、仅在录屏时保护"。
|
||||
*
|
||||
* 实现要点(避免实时画面变黑):
|
||||
* 1) 进入时(录屏开始前)就提前挂好安全层,且默认不保护 → 截屏正常、实时正常;
|
||||
* 2) 仅在录屏期间翻转 isSecureTextEntry = true → 录屏文件黑、实时正常、此刻截屏也黑;
|
||||
* 3) 录屏结束翻回 false → 截屏恢复正常。
|
||||
*/
|
||||
class ScreenRecordTool {
|
||||
// 是否已注册监听
|
||||
static monitoring : boolean = false;
|
||||
|
||||
// 开启防录屏:提前挂载安全层并监听录屏状态变化
|
||||
static onAntiScreenRecord() {
|
||||
DispatchQueue.main.async(execute = () : void => {
|
||||
// 关键:录屏开始前就把安全层挂好(默认不保护)
|
||||
CaptureScreenTool.setupRecordSecureWrapper()
|
||||
if (!this.monitoring) {
|
||||
this.monitoring = true
|
||||
const method = Selector("screenCaptureDidChange")
|
||||
NotificationCenter.default.addObserver(this, selector = method, name = UIScreen.capturedDidChangeNotification, object = null)
|
||||
}
|
||||
// 进入时若已经在录屏,立即按当前状态保护
|
||||
this.applyIfNeeded()
|
||||
})
|
||||
}
|
||||
|
||||
// 录屏状态变化回调
|
||||
@objc static screenCaptureDidChange() {
|
||||
DispatchQueue.main.async(execute = () : void => {
|
||||
this.applyIfNeeded()
|
||||
})
|
||||
}
|
||||
|
||||
// 根据当前录屏状态翻转安全层开关
|
||||
static applyIfNeeded() {
|
||||
CaptureScreenTool.setRecordSecure(UIScreen.main.isCaptured)
|
||||
}
|
||||
|
||||
// 关闭防录屏:移除监听并卸载安全层
|
||||
static offAntiScreenRecord() {
|
||||
DispatchQueue.main.async(execute = () : void => {
|
||||
if (this.monitoring) {
|
||||
NotificationCenter.default.removeObserver(this, name = UIScreen.capturedDidChangeNotification, object = null)
|
||||
this.monitoring = false
|
||||
}
|
||||
CaptureScreenTool.teardownRecordSecureWrapper()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 开启截图监听
|
||||
*/
|
||||
@@ -121,21 +279,46 @@ export const offUserCaptureScreen : OffUserCaptureScreen = function (callback :
|
||||
* 开启/关闭防截屏
|
||||
*/
|
||||
export const setUserCaptureScreen : SetUserCaptureScreen = function (options : SetUserCaptureScreenOptions) {
|
||||
if (UIDevice.current.systemVersion < "13.0") {
|
||||
let res = new SetUserCaptureScreenFailImpl(12001)
|
||||
options.fail?.(res);
|
||||
options.complete?.(res);
|
||||
const systemVersion = UIDevice.current.systemVersion
|
||||
|
||||
} else if (UIDevice.current.systemVersion == "15.1") {
|
||||
let res = new SetUserCaptureScreenFailImpl(12010)
|
||||
options.fail?.(res);
|
||||
options.complete?.(res);
|
||||
} else {
|
||||
if (options.enable == true) {
|
||||
CaptureScreenTool.offAntiScreenshot(options)
|
||||
} else {
|
||||
CaptureScreenTool.onAntiScreenshot(options)
|
||||
if (options.enable == true) {
|
||||
// 允许截屏与录屏:无条件清理录屏遮罩,再移除防截屏遮罩。
|
||||
// 清理动作不受版本限制,避免遗留监听/遮罩。
|
||||
ScreenRecordTool.offAntiScreenRecord()
|
||||
CaptureScreenTool.offAntiScreenshot(options)
|
||||
return
|
||||
}
|
||||
|
||||
if (options.antiRecordOnly == true) {
|
||||
// 仅禁止录屏、允许截屏:进入页面时提前挂好安全层(默认不保护),
|
||||
// 用 UIScreen.isCaptured(iOS 11+)监听录屏/投屏,仅在录屏期间翻转 isSecureTextEntry
|
||||
// —— 实时显示正常、录到的画面为黑屏,且平时不影响截屏。
|
||||
// 注意:secureView 在 iOS 15.1 上不可用,该版本录屏保护会失效(属系统已知问题)。
|
||||
if (systemVersion < "11.0") {
|
||||
let res = new SetUserCaptureScreenFailImpl(12001)
|
||||
options.fail?.(res)
|
||||
options.complete?.(res)
|
||||
return
|
||||
}
|
||||
ScreenRecordTool.onAntiScreenRecord()
|
||||
let res: SetUserCaptureScreenSuccess = {
|
||||
}
|
||||
options.success?.(res)
|
||||
options.complete?.(res)
|
||||
return
|
||||
}
|
||||
|
||||
// 默认:截屏与录屏一起禁止(secureView 方案,受系统版本限制)
|
||||
if (systemVersion < "13.0") {
|
||||
let res = new SetUserCaptureScreenFailImpl(12001)
|
||||
options.fail?.(res)
|
||||
options.complete?.(res)
|
||||
} else if (systemVersion == "15.1") {
|
||||
let res = new SetUserCaptureScreenFailImpl(12010)
|
||||
options.fail?.(res)
|
||||
options.complete?.(res)
|
||||
} else {
|
||||
CaptureScreenTool.onAntiScreenshot(options)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -49,6 +49,12 @@
|
||||
*/
|
||||
enable : boolean;
|
||||
/**
|
||||
* 仅 iOS 生效。仅在 enable 为 false 时有意义:
|
||||
* true: 仅禁止录屏/投屏(录屏时画面被遮挡),允许截屏;
|
||||
* false/不传: 截屏与录屏一起禁止(默认行为)。
|
||||
*/
|
||||
antiRecordOnly ?: boolean;
|
||||
/**
|
||||
* 接口调用成功的回调函数
|
||||
*/
|
||||
// success : SetUserCaptureScreenSuccessCallback | null,
|
||||
|
||||
Reference in New Issue
Block a user