Diary

Diary

日々学んだことをアウトプットする場として初めてみました

各プラットフォームにおけるスクショ防止調査

Netflix 等では、画面スクショ等をしようとした時に、内容が表示されない用画面が真っ黒になります。
自分はこういった機能をみると実装方法を知りたくなるたちなので、少し調べてみました。

[目次]

時間がないひとまとめ

- 各プラットフォームにおけるスクショ・キャプチャ防止機能の実装方法を調査
- Android
    - FLAG_SECURE を window に設定したら一発
- iOS
    - UITextField#isSecureTextEntry を sublayer として使う(トリッキー)
    - capturedDidChangeNotification のイベントを拾う(キャプチャのみ)
- Web(ブラウザ)で実行するには、js では無理そう
    - DRM などのコンテンツ保護を使うしかない

はじめに

Netflix や Hulu などの動画サービスを調べて、各々スクショが禁止されているかを調べました。

コンテンツ保護(真っ黒でスクショされる)

基本的には Android, iOS, web も

スクショ可能

Android

Android で画面録画を禁止するには、WindowManager.LayoutParams#SECURE_FLAG を設定してあげるのが最も簡単で正確です。
(API 1 で追加。)

フラグ操作方法

window に対して設定するには、window#setFlags を使って設定してあげます。

window.setFlags(
    WindowManager.LayoutParams.FLAG_SECURE,
    WindowManager.LayoutParams.FLAG_SECURE,
)

フラグを削除するには、window#clearFlags を呼びます。

window.clearFlags(WindowManager.LayoutParams.FLAG_SECURE)

clearFlags の引数が 1 つであることから想像もできるように、各々のフラグはビット単位で 2**n だけ異なっているため、各フラグの和から、元のフラグが構成できるようになっています。

例えばこんな感じです。

public static final int FLAG_DITHER = 0x00001000;
public static final int FLAG_SECURE = 0x00002000;
public static final int FLAG_SCALED = 0x00004000;

そのため、現在 window に特定のフラグが指定されているかを確認するには、以下のようにします。

fun hasSecureFlag(): Boolean {
    // 今回チェックしたいフラグ: Int 型。
    val FLAG = WindowManager.LayoutParams.FLAG_SECURE
    // 現在設定されているフラグの和: Int 型。
    val flags = window.attributes.flags

    // 論理和を取り、該当ビットが0かどうか判断する。
    return flags and FLAG != 0
}

全体のコードについては、Github を参照ください。

動作確認

SECURE_FLAG を設定すると、スクリーンショット時とスクリーンレコード時に画面が真っ黒になります。
scrcpy 等で画面を他端末に投影した場合も同様です。

なぜか録画を開始すると、スクリーンショット(電源ボタン + 音量下)がうまく撮れない。

すみません、gif が重すぎるんで github へのリンクで。。。

StoryBoard で UITextField#isSecureTextEntry を使う

iOS では userDidTakeScreenshotNotificationcapturedDidChangeNotification を使うと、ユーザーがスクショや画面録画の開始イベントを監視することが可能です。

しかし、通知タイミングがイベント完了であることが致命的に響いてきます。
録画についてはなんとかなるのですが、スクショにおいては完全にイベントが完了しており、画像フォルダに画像が格納された後となります。

かなりトリッキーなのですが、以下に示すように UITextField#isSecureTextEntry を用いて実装できます。

UITextField#isSecureTextEntry

iOS においては、UITextField#isSecureTextEntry の値を true にすることで、テキストを隠したりレコード・ブロードキャストから守る事が可能になります。

A Boolean value that indicates whether a text object disables copying, and in some cases, prevents recording/broadcasting and also hides the text.

subView として UITextField を加える

実は、UITextField#isSecureTextEntry を活用することで、全体のスクリーンショットを防止する事が可能です。
(sublayer を他に持っていない時)

Secure な TextField を sublayer として下敷きにしておくことで、うまくやっているものと思われます。
(難しい。。。)

class ViewController: UIViewController {
    ...

    override func viewDidLoad() {
        super.viewDidLoad()

        testView.makeSecure()
    }
}

extension UIView {
    func makeSecure() {
        DispatchQueue.main.async {
            let bgView = UIView(frame: UIScreen.main.bounds)
            bgView.backgroundColor = .black
            self.superview?.insertSubview(bgView, at: 0)

            let field = UITextField()
            field.isSecureTextEntry = true
            self.addSubview(field)

            field.centerYAnchor.constraint(equalTo: self.centerYAnchor).isActive = true
            field.centerXAnchor.constraint(equalTo: self.centerXAnchor).isActive = true
            self.layer.superlayer?.addSublayer(field.layer)
            field.layer.sublayers?.first?.addSublayer(self.layer)
        }
    }
}

必要に応じて Github もご覧ください。

また、iOS ではこちらの手法を使うことで、特定要素にのみフィルタをかけることが可能かと思われます。

実動

こちらは QuickTime Player を使って対象のアプリを mac で投影した時です。
iphone の画面を mac 上に表示する方法

(おそらく)ブロードキャストモードと判断されているため、画面が無事真っ黒になっています。

SwiftUI

上記では StoryBoard なるものを使いましたが、最近は SwiftUI を使います。

そこで SwiftUI でもやってみようとしたのですが、自分の Swift, SwiftUI 力が足りず、実装できませんでした。
できる方は教えてください!!

SwiftUI でレコーディングを防止

StoryBoard でやったことを SwiftUI に持ってきたかったのですが、実装できず、、、

とりあえず録画の検知のみ実装しました。

capturedDidChangeNotification イベントを検知

capturedDidChangeNotification なるものがあり、これはキャプチャの状態が変更された時(録画開始 or 停止)に発火されるイベントです。

これを swiftUI で検知するには以下のようにします。

ZStack {
    ...
}
.onReceive(NotificationCenter.default.publisher(for: UIScreen.capturedDidChangeNotification)) { _ in
    // イベント検知後の処理。
}

録画中の画面があるかどうか

UIScreen#isCaptured を使って判断します。

A Boolean value that indicates whether the system is actively cloning the screen to another destination.

正確には、画面が他の場所にクローンされているか(ミラーリングされているか?)の判断をしているので、他端末に投影させているかの検知も可能です。

for screen in UIScreen.screens {
    if (screen.isCaptured) {
        // やばい、録画中だった!
    }
}
// 録画中じゃありませんでした。
}

録画中を検知して画面を変更する

ViewModel

import Foundation
import SwiftUI

class MainViewModel: ObservableObject {

    @Published var isRecording = false

    init() {
        refreshIsRecording()
    }

    func refreshIsRecording() -> Void {
        // 録画中(isCaptured)なものがあるか調べる。
        for screen in UIScreen.screens {
            if (screen.isCaptured) {
                isRecording = true
                return
            }
        }
        isRecording = false
    }
}

MainScreen

import SwiftUI

struct ContentView: View {

    @ObservedObject var viewModel = MainViewModel()

    var body: some View {

        ZStack {
            Text("Hello, world!!")
                .padding()

            // レコーディング検出時
            if (viewModel.isRecording) {
                Text("Detect Recording")
                    .foregroundColor(.red)
                    .font(.system(size: 30, weight: .bold))
                    .frame(maxWidth: .infinity, maxHeight: .infinity)
                    .background(.black)
            }
        }
        .ignoresSafeArea(.all)
        .onReceive(NotificationCenter.default.publisher(for: UIScreen.capturedDidChangeNotification)) { _ in
            viewModel.refreshIsRecording()
        }
    }
}

全体のコードは Github にあげてあります。

実動

ちょっとファイルが重すぎたので github へのリンクへ。。。

web

軽く調べた感じでは、javascript を用いてスクショを制御する方法はなさそうです。

以下の違いによる自由度の差かな、とか勝手に想像しています。

  • ハードウェアが基盤となって、その上に OS, application が構築されるモバイル
  • 全てのプラットフォームで使えるように設計された web 通信

そこで DRM や HLS を用いて、コンテンツ自体を保護するようにするのが良さそうです。

おわりに

コンテンツ保護・DRM などの話をはじめて聞いて勉強になりました。
今度それらについても深ぼってやってみたいです!