WebRTC技术研究:Android WebView兼容性问题与Crossover框架实践

记录2019年WebRTC技术研究过程,重点解决在线视频签约应用中Android WebView的兼容性问题和Crossover框架的SSL配置

-- 次阅读

WebRTC技术研究:Android WebView兼容性问题与Crossover框架实践

研究背景

2019年,随着电子签名和远程办公的快速发展,在线视频签约成为金融、法律、房地产等行业的重要需求。在我们的在线视频签约平台项目中,需要实现以下核心功能:

  1. 远程视频签约:支持签约双方实时音视频沟通并完成电子签名
  2. 身份验证:通过视频通话进行人脸识别和身份核验
  3. 合同签署:在视频见证下完成电子合同的签署流程
  4. 过程记录:录制完整的签约过程作为法律证据

项目技术栈背景:

  • 前端:Vue.js + WebRTC + 电子签名SDK
  • 移动端:Android WebView + Crossover框架
  • 后端:Node.js + Socket.IO + WebRTC SFU服务器 + 区块链存证

WebRTC技术概述

1. WebRTC核心组件

视频签约场景的WebRTC架构

┌─────────────────────────────────────┐
│        视频签约应用界面             │
│  (合同展示、电子签名、视频通话)      │
├─────────────────────────────────────┤
│           WebRTC APIs              │
│  (媒体采集、PeerConnection、录制)    │
├─────────────────────────────────────┤
│           移动浏览器               │
│      (Android WebView)             │
├─────────────────────────────────────┤
│           网络传输                 │
│   (STUN/TURN、ICE、网络自适应)      │
└─────────────────────────────────────┘

核心API实现

// 1. 视频签约专用媒体采集
async function getSigningMediaStream() {
    try {
        const constraints = {
            video: {
                width: { ideal: 1280 },
                height: { ideal: 720 },
                frameRate: { ideal: 30 },
                facingMode: "user" // 自拍摄像头
            },
            audio: {
                echoCancellation: true,
                noiseSuppression: true,
                autoGainControl: true,
                // 签约场景需要高清晰度音频
                latency: 0,
                sampleRate: 48000
            }
        };

        const stream = await navigator.mediaDevices.getUserMedia(constraints);

        // 添加屏幕共享用于合同展示(可选)
        if (isContractSharingEnabled) {
            const screenStream = await navigator.mediaDevices.getDisplayMedia({
                video: { cursor: "always" },
                audio: false
            });
            // 合并媒体流
            stream.addTrack(screenStream.getVideoTracks()[0]);
        }

        return stream;
    } catch (error) {
        console.error('获取签约媒体流失败:', error);
        throw new Error('无法启动视频签约,请检查摄像头和麦克风权限');
    }
}

// 2. 签约专用PeerConnection建立
function createSigningPeerConnection() {
    const configuration = {
        iceServers: [
            { urls: 'stun:stun.l.google.com:19302' },
            {
                urls: 'turn:your-turn-server.com:3478',
                username: 'signing-platform',
                credential: 'secure-signing-key'
            }
        ],
        // 签约场景的特殊配置
        bundlePolicy: "max-bundle",
        rtcpMuxPolicy: "require",
        iceTransportPolicy: "all"
    };

    const pc = new RTCPeerConnection(configuration);

    // 处理ICE候选
    pc.onicecandidate = (event) => {
        if (event.candidate) {
            // 发送ICE候选到对方
            sendToSignalingServer({
                type: 'candidate',
                candidate: event.candidate,
                sessionId: getCurrentSessionId()
            });
        }
    };

    // 处理远程流
    pc.ontrack = (event) => {
        const remoteStream = event.streams[0];
        // 显示签约对方视频
        const videoElement = document.getElementById('counterpartyVideo');
        videoElement.srcObject = remoteStream;
        videoElement.play();

        // 开始录制签约过程
        if (isRecordingEnabled) {
            startSessionRecording(remoteStream);
        }
    };

    // 处理数据通道(用于签约状态同步)
    pc.ondatachannel = (event) => {
        const channel = event.channel;
        setupSigningDataChannel(channel);
    };

    return pc;
}

// 3. 签约数据通道设置
function setupSigningDataChannel(channel) {
    channel.onopen = () => {
        console.log('签约数据通道已建立');
        // 发送签约准备状态
        channel.send(JSON.stringify({
            type: 'signing_status',
            status: 'ready',
            timestamp: Date.now()
        }));
    };

    channel.onmessage = (event) => {
        const message = JSON.parse(event.data);
        handleSigningMessage(message);
    };

    channel.onclose = () => {
        console.log('签约数据通道已关闭');
        handleSigninlose();
    };

    channel.onerror = (error) => {
        console.error('签约数据通道错误:', error);
        handleSigningError(error);
    };
}

2. 视频签约信令服务器设计

签约专用信令协议

class SigningSignalingClient {
    constructor(serverUrl) {
        this.ws = new WebSocket(serverUrl);
        this.sessionId = null;
        this.setupEventHandlers();
    }

    setupEventHandlers() {
        this.ws.onopen = () => {
            console.log('签约信令服务器连接成功');
            this.authenticateUser();
        };

        this.ws.onmessage = (event) => {
            const message = JSON.parse(event.data);
            this.handleMessage(message);
        };

        this.ws.onerror = (error) => {
            console.error('签约信令服务器错误:', error);
            this.emit('signaling:error', error);
        };
    }

    authenticateUser() {
        // 发送用户身份验证
        this.send({
            type: 'authenticate',
            userId: getCurrentUserId(),
            sessionId: generateSessionId(),
            timestamp: Date.now()
        });
    }

    handleMessage(message) {
        switch (message.type) {
            case 'authenticated':
                this.sessionId = message.sessionId;
                this.emit('authenticated', message);
                break;
            case 'offer':
                this.handleOffer(message);
                break;
            case 'answer':
                this.handleAnswer(message);
                break;
            case 'candidate':
                this.handleCandidate(message);
                break;
            case 'counterparty_joined':
                this.emit('counterparty:joined', message);
                break;
            case 'contract_signed':
                this.emit('contract:signed', message);
                break;
            case 'session_ended':
                this.emit('session:ended', message);
                break;
        }
    }

    // 发送签约Offer
    sendOffer(offer, contractInfo) {
        this.send({
            type: 'offer',
            sdp: offer,
            contractInfo: contractInfo,
            sessionId: this.sessionId,
            timestamp: Date.now()
        });
    }

    // 发送签约Answer
    sendAnswer(answer) {
        this.send({
            type: 'answer',
            sdp: answer,
            sessionId: this.sessionId,
            timestamp: Date.now()
        });
    }

    // 发送ICE Candidate
    sendCandidate(candidate) {
        this.send({
            type: 'candidate',
            candidate: candidate,
            sessionId: this.sessionId
        });
    }

    // 发送合同签署状态
    sendContractStatus(status, documentId, signature) {
        this.send({
            type: 'contract_status',
            status: status,
            documentId: documentId,
            signature: signature,
            sessionId: this.sessionId,
            timestamp: Date.now()
        });
    }

    // 结束签约会话
    endSession(reason) {
        this.send({
            type: 'end_session',
            reason: reason,
            sessionId: this.sessionId,
            timestamp: Date.now()
        });
    }
}

Android WebView兼容性问题

1. 签约场景权限配置

问题分析: 在线视频签约对权限要求更高,需要确保摄像头、麦克风、存储等权限的正确配置。

解决方案

class SigningWebRTCAwareWebView : WebView {
    constructor(context: Context) : super(context) {
        initWebView()
    }

    constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) {
        initWebView()
    }

    private fun initWebView() {
        // 启用JavaScript和媒体权限
        settings.javaScriptEnabled = true
        settings.mediaPlaybackRequiresUserGesture = false
        settings.domStorageEnabled = true
        settings.databaseEnabled = true

        // 签约场景特殊配置
        settings.allowUniversalAccessFromFileURLs = true
        settings.allowFileAccessFromFileURLs = true

        // 设置签约专用用户代理
        settings.userAgentString = "${settings.userAgentString} VideoSigningApp/1.0"

        // 启用混合内容(HTTPS页面加载HTTP资源)
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
            settings.mixedContentMode = WebSettings.MIXED_CONTENT_ALWAYS_ALLOW
        }

        webViewClient = SigningWebViewClient()
        webChromeClient = SigningWebChromeClient()
    }

    private inner class SigningWebViewClient : WebViewClient() {
        override fun onReceivedSslError(view: WebView?, handler: SslErrorHandler?, error: SslError?) {
            // 签约场景SSL处理
            when (error?.primaryError) {
                SslError.SSL_UNTRUSTED -> {
                    if (BuildConfig.DEBUG || isTrustedCertificate(error)) {
                        handler?.proceed()
                    } else {
                        showSigningSSLError("证书不受信任,无法保证签约安全")
                        handler?.cancel()
                    }
                }
                else -> {
                    showSigningSSLError("SSL证书验证失败")
                    handler?.cancel()
                }
            }
        }

        private fun isTrustedCertificate(error: SslError?): Boolean {
            // 检查是否为签约平台信任的证书
            return true // 实际实现中需要具体的证书验证逻辑
        }

        private fun showSigningSSLError(message: String) {
            AlertDialog.Builder(context)
                .setTitle("签约安全警告")
                .setMessage(message)
                .setPositiveButton("退出签约") { _, _ ->
                    // 结束签约会话
                    (context as Activity).finish()
                }
                .setNegativeButton("继续") { _, _ ->
                    // 在某些情况下允许继续
                }
                .show()
        }
    }

    private inner class SigningWebChromeClient : WebChromeClient() {
        override fun onPermissionRequest(request: PermissionRequest?) {
            val resources = request?.resources ?: return

            val grantedResources = mutableListOf<String>()

            for (resource in resources) {
                when (resource) {
                    PermissionRequest.RESOURCE_VIDEO_CAPTURE -> {
                        if (hasCameraPermission() && verifyCameraForSigning()) {
                            grantedResources.add(PermissionRequest.RESOURCE_VIDEO_CAPTURE)
                        }
                    }
                    PermissionRequest.RESOURCE_AUDIO_CAPTURE -> {
                        if (hasAudioPermission() && verifyAudioForSigning()) {
                            grantedResources.add(PermissionRequest.RESOURCE_AUDIO_CAPTURE)
                        }
                    }
                    PermissionRequest.RESOURCE_PROTECTED_MEDIA_ID -> {
                        if (hasMediaPermission()) {
                            grantedResources.add(PermissionRequest.RESOURCE_PROTECTED_MEDIA_ID)
                        }
                    }
                }
            }

            if (grantedResources.size == resources.size) {
                request.grant(grantedResources.toTypedArray())
            } else {
                request.deny()
            }
        }
    }

    private fun verifyCameraForSigning(): Boolean {
        // 验证摄像头是否适合签约场景
        return try {
            val cameraManager = context.getSystemService(Context.CAMERA_SERVICE) as CameraManager
            val cameraId = cameraManager.cameraIdList[0]
            val characteristics = cameraManager.getCameraCharacteristics(cameraId)

            // 检查摄像头质量是否满足签约要求
            val capabilities = characteristics.get(CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES)
            capabilities?.contains(CameraMetadata.REQUEST_AVAILABLE_CAPABILITIES_BACKWARD_COMPATIBLE) == true
        } catch (e: Exception) {
            false
        }
    }

    private fun verifyAudioForSigning(): Boolean {
        // 验证麦克风是否适合签约场景
        return ContextCompat.checkSelfPermission(
            context,
            Manifest.permission.RECORD_AUDIO
        ) == PackageManager.PERMISSION_GRANTED
    }

    private fun hasMediaPermission(): Boolean {
        return ContextCompat.checkSelfPermission(
            context,
            Manifest.permission.WRITE_EXTERNAL_STORAGE
        ) == PackageManager.PERMISSION_GRANTED
    }
}

2. 签约场景WebRTC兼容性

问题:Android WebView对WebRTC支持不完整,影响签约体验

解决方案

// 视频签约WebRTC兼容性检测
class VideoSigningWebRTCSupportChecker {
    static checkSupport() {
        const support = {
            getUserMedia: !!(navigator.mediaDevices && navigator.mediaDevices.getUserMedia),
            peerConnection: !!window.RTCPeerConnection,
            dataChannel: !!window.RTCDataChannel,
            mediaRecording: !!window.MediaRecorder,
            screenCapture: !!(navigator.mediaDevices && navigator.mediaDevices.getDisplayMedia),
            // 签约特殊需求
            highQualityVideo: this.supportsHighQualityVideo(),
            echoCancellation: this.supportsEchoCancellation(),
            noiseSuppression: this.supportsNoiseSuppression()
        };

        return support;
    }

    static supportsHighQualityVideo() {
        try {
            const canvas = document.createElement('canvas');
            canvas.width = 1920;
            canvas.height = 1080;
            return canvas.toDataURL().length > 0;
        } catch (e) {
            return false;
        }
    }

    static supportsEchoCancellation() {
        return typeof MediaStreamTrackProcessor !== 'undefined';
    }

    static supportsNoiseSuppression() {
        return typeof AudioContext !== 'undefined';
    }

    static loadSigningPolyfills() {
        // 加载签约专用polyfill
        const scripts = [
            'https://webrtc.github.io/adapter/adapter-latest.js',
            '/js/signing-webrtc-polyfill.js' // 签约专用polyfill
        ];

        scripts.forEach(src => {
            const script = document.createElement('script');
            script.src = src;
            document.head.appendChild(script);
        });
    }
}

// 签约专用WebRTC初始化
class VideoSigningWebRTCManager {
    constructor() {
        this.localStream = null;
        this.remoteStream = null;
        this.peerConnection = null;
        this.signalingClient = null;
        this.recordingManager = null;
        this.contractManager = null;
    }

    async initialize() {
        // 检查兼容性
        const support = VideoSigningWebRTCSupportChecker.checkSupport();
        if (!support.getUserMedia) {
            throw new Error('您的设备不支持视频签约,请升级到最新版本的浏览器');
        }

        // 加载polyfill
        VideoSigningWebRTCSupportChecker.loadSigningPolyfills();

        // 初始化组件
        await this.initializeSignaling();
        await this.initializeContract();
    }

    async initializeSignaling() {
        this.signalingClient = new SigningSignalingClient(this.getSignalingServerUrl());
        this.signalingClient.on('authenticated', () => {
            console.log('签约信令认证成功');
        });
    }

    async initializeContract() {
        this.contractManager = new ContractManager();
        await this.contractManager.initialize();
    }

    async startSigningSession(contractInfo) {
        try {
            // 获取高质量媒体流
            this.localStream = await this.getHighQualityMediaStream();

            // 创建签约专用PeerConnection
            this.peerConnection = this.createSigningPeerConnection();

            // 添加本地流
            this.localStream.getTracks().forEach(track => {
                this.peerConnection.addTrack(track, this.localStream);
            });

            // 创建Offer
            const offer = await this.peerConnection.createOffer();
            await this.peerConnection.setLocalDescription(offer);

            // 发送签约邀请
            this.signalingClient.sendOffer(offer, contractInfo);

            return offer;
        } catch (error) {
            console.error('启动签约会话失败:', error);
            throw new Error('无法启动视频签约会话');
        }
    }

    async getHighQualityMediaStream() {
        const constraints = {
            video: {
                width: { ideal: 1920, max: 1920 },
                height: { ideal: 1080, max: 1080 },
                frameRate: { ideal: 30, max: 30 },
                facingMode: "user"
            },
            audio: {
                echoCancellation: true,
                noiseSuppression: true,
                autoGainControl: true,
                latency: 0,
                sampleRate: 48000,
                channelCount: 2
            }
        };

        return await navigator.mediaDevices.getUserMedia(constraints);
    }
}

Crossover框架集成

1. 签约场景Crossover配置

Android端签约功能集成

class VideoSigningActivity : AppCompatActivity() {
    private lateinit var webView: SigningWebRTCAwareWebView
    private lateinit var crossover: Crossover
    private lateinit var signingManager: VideoSigningManager
    private var currentSessionId: String? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_video_signing)

        initWebView()
        initCrossover()
        initSigningManager()
        loadSigningPage()
    }

    private fun initWebView() {
        webView = findViewById(R.id.webView)
    }

    private fun initCrossover() {
        crossover = Crossover.Builder()
            .withWebView(webView)
            .withLogging(BuildConfig.DEBUG)
            .build()

        registerSigningMethods()
    }

    private fun initSigningManager() {
        signingManager = VideoSigningManager(this)
        signingManager.setOnSessionStateChangeListener { state ->
            handleSessionStateChange(state)
        }
    }

    private fun registerSigningMethods() {
        // 获取签约设备信息
        crossover.register("getSigningDeviceInfo") { params ->
            val deviceInfo = JSONObject().apply {
                put("platform", "Android")
                put("version", Build.VERSION.RELEASE)
                put("model", Build.MODEL)
                put("manufacturer", Build.MANUFACTURER)
                put("isFrontCameraAvailable", isFrontCameraAvailable())
                put("isAudioQualityGood", isAudioQualityGood())
            }
            CompletableFuture.completedFuture(deviceInfo)
        }

        // 检查签约权限
        crossover.register("checkSigningPermissions") { params ->
            val hasCamera = hasCameraPermission()
            val hasAudio = hasAudioPermission()
            val hasStorage = hasStoragePermission()
            val result = JSONObject().apply {
                put("camera", hasCamera)
                put("audio", hasAudio)
                put("storage", hasStorage)
                put("allGranted", hasCamera && hasAudio && hasStorage)
            }
            CompletableFuture.completedFuture(result)
        }

        // 开始签约会话
        crossover.register("startSigningSession") { params ->
            val contractId = params.getString("contractId")
            val sessionId = startSigningSession(contractId)
            CompletableFuture.completedFuture(sessionId)
        }

        // 结束签约会话
        crossover.register("endSigningSession") { params ->
            endCurrentSession()
            CompletableFuture.completedFuture(true)
        }

        // 电子签名集成
        crossover.register("performElectronicSignature") { params ->
            val documentId = params.getString("documentId")
            val signatureData = params.getString("signatureData")
            val result = performElectronicSignature(documentId, signatureData)
            CompletableFuture.completedFuture(result)
        }

        // 身份验证
        crossover.register("verifyIdentity") { params ->
            val idCardNumber = params.getString("idCardNumber")
            val realName = params.getString("realName")
            val result = verifyIdentity(idCardNumber, realName)
            CompletableFuture.completedFuture(result)
        }

        // 录制签约过程
        crossover.register("startRecording") { params ->
            val recordingPath = startSessionRecording()
            CompletableFuture.completedFuture(recordingPath)
        }

        // 停止录制
        crossover.register("stopRecording") { params ->
            val recordingPath = stopSessionRecording()
            CompletableFuture.completedFuture(recordingPath)
        }
    }

    private fun startSigningSession(contractId: String): String {
        currentSessionId = generateSessionId()
        signingManager.startSession(currentSessionId!!, contractId)
        return currentSessionId!!
    }

    private fun endCurrentSession() {
        currentSessionId?.let { sessionId ->
            signingManager.endSession(sessionId)
            currentSessionId = null
        }
    }

    private fun performElectronicSignature(documentId: String, signatureData: String): Boolean {
        return try {
            val signatureResult = signingManager.performSignature(documentId, signatureData)
            crossover.call("onSignatureCompleted", mapOf(
                "documentId" to documentId,
                "success" to signatureResult.success,
                "signatureId" to signatureResult.signatureId
            ))
            signatureResult.success
        } catch (e: Exception) {
            crossover.call("onSignatureFailed", mapOf(
                "documentId" to documentId,
                "error" to e.message
            ))
            false
        }
    }

    private fun verifyIdentity(idCardNumber: String, realName: String): Boolean {
        return try {
            val verificationResult = signingManager.verifyIdentity(idCardNumber, realName)
            crossover.call("onIdentityVerified", mapOf(
                "success" to verificationResult.success,
                "verifiedName" to verificationResult.verifiedName,
                "confidence" to verificationResult.confidence
            ))
            verificationResult.success
        } catch (e: Exception) {
            crossover.call("onIdentityVerificationFailed", mapOf(
                "error" to e.message
            ))
            false
        }
    }

    private fun startSessionRecording(): String {
        return signingManager.startRecording(currentSessionId!!)
    }

    private fun stopSessionRecording(): String {
        return signingManager.stopRecording()
    }

    private fun handleSessionStateChange(state: SessionState) {
        when (state) {
            SessionState.CONNECTING -> {
                showLoading("正在连接签约对方面...")
            }
            SessionState.CONNECTED -> {
                hideLoading()
                crossover.call("onCounterpartyConnected")
            }
            SessionState.FAILED -> {
                showError("签约连接失败")
                crossover.call("onSessionFailed")
            }
            SessionState.ENDED -> {
                showSuccess("签约会话已结束")
                crossover.call("onSessionEnded")
            }
        }
    }

    private fun isFrontCameraAvailable(): Boolean {
        val cameraManager = getSystemService(Context.CAMERA_SERVICE) as CameraManager
        return try {
            cameraManager.cameraIdList.any { id ->
                val characteristics = cameraManager.getCameraCharacteristics(id)
                val lensFacing = characteristics.get(CameraCharacteristics.LENS_FACING)
                lensFacing == CameraMetadata.LENS_FACING_FRONT
            }
        } catch (e: Exception) {
            false
        }
    }

    private fun isAudioQualityGood(): Boolean {
        return true // 实际实现中需要具体的音频质量检测
    }

    private fun hasStoragePermission(): Boolean {
        return ContextCompat.checkSelfPermission(
            this,
            Manifest.permission.WRITE_EXTERNAL_STORAGE
        ) == PackageManager.PERMISSION_GRANTED
    }

    private fun loadSigningPage() {
        val contractId = intent.getStringExtra("contractId") ?: ""
        val url = "https://your-signing-platform.com/signing?contractId=$contractId"
        webView.loadUrl(url)
    }

    companion object {
        private const val REQUEST_PERMISSIONS_CODE = 1001
    }
}

JavaScript端签约功能

class VideoSigningWebRTCManager {
    constructor() {
        this.crossover = null;
        this.peerConnection = null;
        this.localStream = null;
        this.signalingClient = null;
        this.contractInfo = null;
        this.sessionId = null;
    }

    async initialize() {
        // 初始化Crossover
        this.crossover = await Crossover.initialize();

        // 检查签约设备权限
        const permissions = await this.crossover.call('checkSigningPermissions');
        if (!permissions.allGranted) {
            await this.requestSigningPermissions();
        }

        // 初始化信令客户端
        this.signalingClient = new SigningSignalingClient(this.getSignalingServerUrl());

        // 设置事件监听
        this.setupEventListeners();
    }

    async requestSigningPermissions() {
        try {
            await this.crossover.call('requestSigningPermissions');
            console.log('签约权限请求成功');
        } catch (error) {
            console.error('签约权限请求失败:', error);
            throw new Error('无法获取签约所需的设备权限');
        }
    }

    setupEventListeners() {
        // 监听签约会话状态变化
        this.crossover.on('onCounterpartyConnected', () => {
            this.emit('counterparty:connected');
            showSuccess('签约对方已连接');
        });

        this.crossover.on('onSessionFailed', () => {
            this.emit('session:failed');
            showError('签约连接失败,请重试');
        });

        this.crossover.on('onSessionEnded', () => {
            this.emit('session:ended');
            showSuccess('签约会话已结束');
        });

        // 监听签名完成事件
        this.crossover.on('onSignatureCompleted', (data) => {
            this.emit('signature:completed', data);
            showSuccess('电子签名已完成');
        });

        this.crossover.on('onSignatureFailed', (data) => {
            this.emit('signature:failed', data);
            showError('电子签名失败: ' + data.error);
        });

        // 监听身份验证结果
        this.crossover.on('onIdentityVerified', (data) => {
            this.emit('identity:verified', data);
            if (data.success) {
                showSuccess(`身份验证通过 (${data.verifiedName})`);
            }
        });

        this.crossover.on('onIdentityVerificationFailed', (data) => {
            this.emit('identity:failed', data);
            showError('身份验证失败: ' + data.error);
        });

        // 监听录制事件
        this.crossover.on('onRecordingStarted', (data) => {
            this.emit('recording:started', data);
            showInfo('签约过程开始录制');
        });

        this.crossover.on('onRecordingStopped', (data) => {
            this.emit('recording:stopped', data);
            showSuccess('签约录制已完成');
        });
    }

    async startSigningSession(contractInfo) {
        try {
            // 获取本地媒体流
            this.localStream = await this.getSigningMediaStream();

            // 创建PeerConnection
            this.peerConnection = this.createSigningPeerConnection();

            // 添加本地流
            this.localStream.getTracks().forEach(track => {
                this.peerConnection.addTrack(track, this.localStream);
            });

            // 创建Offer
            const offer = await this.peerConnection.createOffer();
            await this.peerConnection.setLocalDescription(offer);

            // 开始签约会话
            this.sessionId = await this.crossover.call('startSigningSession', {
                contractId: contractInfo.id
            });

            // 发送Offer到签约对方
            this.signalingClient.sendOffer(offer, contractInfo);

            return offer;
        } catch (error) {
            console.error('启动签约会话失败:', error);
            throw error;
        }
    }

    async performElectronicSignature(documentId, signatureData) {
        try {
            const result = await this.crossover.call('performElectronicSignature', {
                documentId: documentId,
                signatureData: signatureData
            });

            if (result) {
                // 签名成功,继续后续流程
                this.handleSignatureSuccess(documentId);
            } else {
                throw new Error('电子签名失败');
            }

            return result;
        } catch (error) {
            console.error('执行电子签名失败:', error);
            throw error;
        }
    }

    async verifyIdentity(idCardNumber, realName) {
        try {
            const result = await this.crossover.call('verifyIdentity', {
                idCardNumber: idCardNumber,
                realName: realName
            });

            return result;
        } catch (error) {
            console.error('身份验证失败:', error);
            throw error;
        }
    }

    async startSessionRecording() {
        try {
            const recordingPath = await this.crossover.call('startRecording');
            return recordingPath;
        } catch (error) {
            console.error('开始录制失败:', error);
            throw error;
        }
    }

    async stopSessionRecording() {
        try {
            const recordingPath = await this.crossover.call('stopRecording');
            return recordingPath;
        } catch (error) {
            console.error('停止录制失败:', error);
            throw error;
        }
    }

    endSigningSession() {
        if (this.peerConnection) {
            this.peerConnection.close();
            this.peerConnection = null;
        }

        if (this.localStream) {
            this.localStream.getTracks().forEach(track => track.stop());
            this.localStream = null;
        }

        if (this.sessionId) {
            this.crossover.call('endSigningSession');
            this.sessionId = null;
        }
    }
}

SSL问题解决

1. 签约场景SSL证书配置

问题:视频签约涉及敏感信息,对SSL安全性要求极高

解决方案

Android端签约SSL处理

class SigningSSLUtils {
    companion object {
        fun getSigningSSLSocketFactory(): SSLSocketFactory {
            try {
                // 创建信任管理器
                val trustAllCerts = arrayOf<TrustManager>(object : X509TrustManager {
                    override fun checkClientTrusted(chain: Array<X509Certificate>, authType: String) {
                        // 签约场景:严格验证客户端证书
                        validateClientCertificate(chain)
                    }

                    override fun checkServerTrusted(chain: Array<X509Certificate>, authType: String) {
                        // 签约场景:严格验证服务器证书
                        validateServerCertificate(chain)
                    }

                    override fun getAcceptedIssuers(): Array<X509Certificate> {
                        return arrayOf()
                    }
                })

                val sslContext = SSLContext.getInstance("TLSv1.2")
                sslContext.init(null, trustAllCerts, SecureRandom())

                return sslContext.socketFactory
            } catch (e: Exception) {
                throw RuntimeException("签约SSL配置失败: ${e.message}")
            }
        }

        private fun validateClientCertificate(chain: Array<X509Certificate>) {
            // 签约客户端证书验证逻辑
            if (chain.isEmpty()) {
                throw CertificateException("客户端证书链为空")
            }

            // 验证证书有效期
            val cert = chain[0]
            cert.checkValidity()

            // 验证证书颁发机构
            if (!isTrustedCA(cert.issuerDN.name)) {
                throw CertificateException("客户端证书由不受信任的CA颁发")
            }
        }

        private fun validateServerCertificate(chain: Array<X509Certificate>) {
            // 签约服务器证书验证逻辑
            if (chain.isEmpty()) {
                throw CertificateException("服务器证书链为空")
            }

            // 验证证书有效期
            val cert = chain[0]
            cert.checkValidity()

            // 验证证书域名匹配
            if (!isDomainMatch(cert)) {
                throw CertificateException("服务器证书域名不匹配")
            }

            // 验证证书链
            validateCertificateChain(chain)
        }

        private fun isTrustedCA(issuerName: String): Boolean {
            // 检查是否为签约平台信任的CA
            val trustedCAs = listOf(
                "CN=GlobalSign Root CA,OU=Root CA,O=GlobalSign nv-sa,C=BE",
                "CN=DigiCert TLS RSA SHA256 2020 CA1,O=DigiCert Inc,C=US"
            )
            return trustedCAs.any { trusted -> issuerName.contains(trusted) }
        }

        private fun isDomainMatch(cert: X509Certificate): Boolean {
            // 验证证书域名是否匹配签约平台域名
            val signingDomains = listOf(
                "signing-platform.com",
                "secure-signing.com",
                "video-sign.com"
            )

            // 简化实现:检查Subject Alternative Name
            return signingDomains.any { domain ->
                cert.subjectDN.name.contains(domain)
            }
        }

        private fun validateCertificateChain(chain: Array<X509Certificate>) {
            // 验证证书链的完整性和可信度
            for (i in 0 until chain.size - 1) {
                try {
                    chain[i].verify(chain[i + 1].publicKey)
                } catch (e: Exception) {
                    throw CertificateException("证书链验证失败: ${e.message}")
                }
            }
        }

        fun getSigningHostnameVerifier(): HostnameVerifier {
            return HostnameVerifier { hostname, session ->
                // 签约专用域名验证
                val signingHostnames = setOf(
                    "signing-platform.com",
                    "secure-signing.com",
                    "video-sign.com",
                    "localhost" // 开发环境
                )

                signingHostnames.any { hostname.endsWith(it) }
            }
        }
    }
}

签约WebView SSL错误处理

class SigningWebViewClient : WebViewClient() {
    override fun onReceivedSslError(view: WebView?, handler: SslErrorHandler?, error: SslError?) {
        when (error?.primaryError) {
            SslError.SSL_UNTRUSTED -> {
                // 签约场景:证书不受信任是严重安全问题
                showSigningSecurityAlert(
                    "证书不受信任",
                    "检测到不受信任的SSL证书,这可能危及您的签约安全。请不要继续操作。"
                )
                handler?.cancel()
            }
            SslError.SSL_EXPIRED -> {
                // 签约场景:过期证书不允许继续
                showSigningSecurityAlert(
                    "证书已过期",
                    "SSL证书已过期,无法保证签约过程的安全性。"
                )
                handler?.cancel()
            }
            SslError.SSL_IDMISMATCH -> {
                // 签约场景:域名不匹配需要严格处理
                showSigningSecurityAlert(
                    "证书域名不匹配",
                    "SSL证书域名与签约平台域名不匹配,可能存在安全风险。"
                )
                handler?.cancel()
            }
            SslError.SSL_NOTYETVALID -> {
                // 签约场景:证书未生效
                showSigningSecurityAlert(
                    "证书无效",
                    "SSL证书尚未生效,无法建立安全连接。"
                )
                handler?.cancel()
            }
            else -> {
                showSigningSecurityAlert(
                    "SSL证书错误",
                    "SSL证书验证过程中发生未知错误,为保证签约安全,请不要继续操作。"
                )
                handler?.cancel()
            }
        }
    }

    private fun showSigningSecurityAlert(title: String, message: String) {
        AlertDialog.Builder(view.context)
            .setTitle("签约安全警告")
            .setIcon(R.drawable.ic_security_warning)
            .setMessage(message)
            .setPositiveButton("退出签约") { _, _ ->
                // 记录安全事件
                SecurityLogger.logSSLError(title, message)
                // 结束签约会话
                (view.context as Activity).finish()
            }
            .setNegativeButton("查看证书") { _, _ ->
                // 显示证书详细信息
                showCertificateDetails()
            }
            .setCancelable(false) // 签约场景不允许忽略安全警告
            .show()
    }

    private fun showCertificateDetails() {
        // 显示SSL证书详细信息的对话框
        // 包括证书颁发者、有效期、域名等信息
    }
}

2. 签约数据加密

问题:签约过程中的敏感数据需要额外加密保护

解决方案

class SigningDataEncryption {
    companion object {
        private const val TRANSFORMATION = "AES/GCM/NoPadding"
        private const val KEY_SIZE = 256
        private const val IV_SIZE = 12

        fun encryptSignData(data: String, sessionId: String): String {
            return try {
                val key = generateSessionKey(sessionId)
                val cipher = Cipher.getInstance(TRANSFORMATION)
                val iv = generateIv()
                cipher.init(Cipher.ENCRYPT_MODE, key, iv)

                val encryptedData = cipher.doFinal(data.toByteArray(Charsets.UTF_8))
                val ivBytes = iv.iv

                // 将IV和加密数据合并
                val combinedData = ByteArray(ivBytes.size + encryptedData.size)
                System.arraycopy(ivBytes, 0, combinedData, 0, ivBytes.size)
                System.arraycopy(encryptedData, 0, combinedData, ivBytes.size, encryptedData.size)

                // Base64编码
                android.util.Base64.encodeToString(combinedData, android.util.Base64.DEFAULT)
            } catch (e: Exception) {
                throw EncryptionException("签约数据加密失败", e)
            }
        }

        fun decryptSignData(encryptedData: String, sessionId: String): String {
            return try {
                val combinedData = android.util.Base64.decode(encryptedData, android.util.Base64.DEFAULT)
                val key = generateSessionKey(sessionId)

                // 提取IV
                val iv = IvParameterSpec(combinedData, 0, IV_SIZE / 8)

                // 提取加密数据
                val encryptedBytes = ByteArray(combinedData.size - IV_SIZE / 8)
                System.arraycopy(combinedData, IV_SIZE / 8, encryptedBytes, 0, encryptedBytes.size)

                val cipher = Cipher.getInstance(TRANSFORMATION)
                cipher.init(Cipher.DECRYPT_MODE, key, iv)

                val decryptedData = cipher.doFinal(encryptedBytes)
                String(decryptedData, Charsets.UTF_8)
            } catch (e: Exception) {
                throw DecryptionException("签约数据解密失败", e)
            }
        }

        private fun generateSessionKey(sessionId: String): SecretKey {
            val keyMaterial = ("SigningKey" + sessionId + BuildConfig.SIGNING_SECRET_KEY).toByteArray(Charsets.UTF_8)
            val sha256 = MessageDigest.getInstance("SHA-256")
            val keyHash = sha256.digest(keyMaterial)

            val keyBytes = ByteArray(KEY_SIZE / 8)
            System.arraycopy(keyHash, 0, keyBytes, 0, keyBytes.size)

            return SecretKeySpec(keyBytes, "AES")
        }

        private fun generateIv(): SecureRandom {
            val iv = ByteArray(IV_SIZE / 8)
            SecureRandom().nextBytes(iv)
            return SecureRandom(iv)
        }
    }
}

// 签约异常类
class EncryptionException(message: String, cause: Throwable) : Exception(message, cause)
class DecryptionException(message: String, cause: Throwable) : Exception(message, cause)

实际应用案例

1. 在线视频签约平台集成

签约平台架构

┌─────────────────┐    ┌──────────────────┐    ┌─────────────────┐
│   Web前端       │    │   Android App    │    │   签约后端      │
│  (Vue.js)       │<──>│   (WebView)      │<──>│   (Node.js)     │
│                 │    │                   │    │                 │
│ • 合同展示      │    │ • WebView容器     │    │ • 信令服务器    │
│ • 视频签约      │    │ • Crossover桥接   │    │ • 区块链存证    │
│ • 电子签名      │    │ • 权限管理        │    │ • 身份验证      │
│ • 录制回放      │    │ • SSL安全处理     │    │ • 数据加密      │
└─────────────────┘    └──────────────────┘    └─────────────────┘

核心签约流程

class VideoSigningWorkflow {
    private var signingSession: SigningSession? = null
    private var contractManager: ContractManager? = null
    private var identityVerifier: IdentityVerifier? = null
    private var recordingManager: RecordingManager? = null

    suspend fun startCompleteSigningProcess(contractId: String, userId: String) {
        try {
            // 1. 合同验证
            val contract = verifyContract(contractId)

            // 2. 用户身份验证
            val identityResult = verifyUserIdentity(userId)

            // 3. 启动视频签约会话
            val session = startVideoSigningSession(contract, userId)

            // 4. 视频验证和沟通
            val videoVerificationResult = performVideoVerification(session)

            // 5. 电子签名
            val signatureResult = performElectronicSignature(session, contract)

            // 6. 录制和存证
            val evidenceResult = createSigningEvidence(session, contract)

            // 7. 完成签约
            completeSigningProcess(session, evidenceResult)

        } catch (e: Exception) {
            handleSigningError(e)
            throw e
        }
    }

    private suspend fun verifyContract(contractId: String): Contract {
        // 合同真实性验证
        return contractManager?.verifyContract(contractId)
            ?: throw IllegalStateException("合同管理器未初始化")
    }

    private suspend fun verifyUserIdentity(userId: String): IdentityVerificationResult {
        // 用户身份验证
        return identityVerifier?.verifyIdentity(userId)
            ?: throw IllegalStateException("身份验证器未初始化")
    }

    private suspend fun startVideoSigningSession(contract: Contract, userId: String): SigningSession {
        // 启动视频签约会话
        val session = SigningSession(
            sessionId = generateSessionId(),
            contract = contract,
            userId = userId,
            startTime = System.currentTimeMillis()
        )

        // 通知前端开始签约
        notifyFrontend("signing:session:started", session)

        return session
    }

    private suspend fun performVideoVerification(session: SigningSession): VideoVerificationResult {
        // 视频身份验证
        return identityVerifier?.performVideoVerification(session.sessionId)
            ?: throw IllegalStateException("身份验证器未初始化")
    }

    private suspend fun performElectronicSignature(session: SigningSession, contract: Contract): SignatureResult {
        // 电子签名流程
        val signatureData = collectSignatureData()
        val signatureResult = signingManager.performSignature(contract.id, signatureData)

        // 通知前端签名完成
        notifyFrontend("signing:signature:completed", signatureResult)

        return signatureResult
    }

    private suspend fun createSigningEvidence(session: SigningSession, contract: Contract): SigningEvidence {
        // 创建签约证据
        val recordingPath = recordingManager?.stopRecording() ?: ""

        return SigningEvidence(
            sessionId = session.sessionId,
            contractId = contract.id,
            recordingPath = recordingPath,
            signatureId = session.signatureResult?.signatureId,
            verificationResult = session.videoVerificationResult,
            timestamp = System.currentTimeMillis(),
            blockchainHash = createBlockchainEvidence(recordingPath)
        )
    }

    private suspend fun completeSigningProcess(session: SigningSession, evidence: SigningEvidence) {
        // 完成签约流程
        session.endTime = System.currentTimeMillis()
        session.evidence = evidence

        // 保存签约记录
        saveSigningRecord(session)

        // 通知前端签约完成
        notifyFrontend("signing:completed", session)

        // 发送确认邮件/短信
        sendConfirmationNotification(session)
    }
}

2. 性能优化

签约场景性能调优

class VideoSigningPerformanceOptimizer {
    companion object {
        fun optimizeSigningPeerConnectionConfig(): PeerConnectionConfiguration {
            return PeerConnectionConfiguration().apply {
                // 签约场景的ICE配置优化
                iceServers = listOf(
                    IceServer("stun:stun.l.google.com:19302"),
                    IceServer("turn:signing-turn-server.com:3478", "signing-platform", "secure-turn-key")
                )

                // 签约场景的传输策略
                bundlePolicy = BundlePolicy.MAX_BUNDLE
                rtcpMuxPolicy = RtcpMuxPolicy.REQUIRE

                // 签约场景的编码器配置
                videoCodec = VideoCodec.H264 // 签约场景优先选择H264保证兼容性
                audioCodec = AudioCodec.OPUS

                // 签约场景的带宽限制
                maxVideoBitrate = 1500 // kbps - 签约需要更高清晰度
                maxAudioBitrate = 256 // kbps - 签约需要更高音频质量

                // 签约场景的延迟优化
                enableDscp = true
                enableCpuOveruseDetection = true
                enableVideoFrameDrop = false // 签约不允许丢帧
            }
        }

        fun optimizeSigningMediaConstraints(): MediaConstraints {
            return MediaConstraints().apply {
                // 签约场景的视频约束
                video = MediaConstraints.VideoConstraints().apply {
                    minWidth = 1280
                    maxWidth = 1920
                    minHeight = 720
                    maxHeight = 1080
                    minFrameRate = 24
                    maxFrameRate = 30
                    // 签约场景需要更高的视频质量
                    enableHighQuality = true
                }

                // 签约场景的音频约束
                audio = MediaConstraints.AudioConstraints().apply {
                    echoCancellation = true
                    noiseSuppression = true
                    autoGainControl = true
                    highPassFilter = true
                    // 签约场景的特殊音频处理
                    enableNoiseSuppression = true
                    enableEchoCancellation = true
                }
            }
        }

        fun optimizeSigningNetworkConfiguration(): NetworkConfiguration {
            return NetworkConfiguration().apply {
                // 签约场景的网络超时配置
                connectionTimeout = 15000 // 15秒 - 签约需要更长的连接时间
                signalingTimeout = 10000 // 10秒
                iceCandidateTimeout = 30000 // 30秒 - 签约需要更长的ICE收集时间

                // 签约场景的重连策略
                maxReconnectionAttempts = 5 // 签约允许更多重连尝试
                reconnectionDelay = 3000 // 3秒重连间隔
                enableFastReconnection = true
            }
        }
    }
}

技术栈总结(2019年11月)

前端技术栈:
Vue.js 2.6
WebRTC API
Socket.IO 2.3
Crossover 1.0
电子签名SDK

Android技术栈:
Android SDK 28 (9.0)
WebView + WebRTC
Crossover Bridge
OkHttp 3.14
Retrofit 2.6
安全加密库

后端技术栈:
Node.js 12.x
WebRTC SFU Server
Socket.IO Server
区块链存证服务
身份验证服务

经验总结

1. 技术收获

WebRTC深度理解

  • 掌握了WebRTC在金融级应用中的安全要求
  • 理解了视频签约场景的特殊性能需求
  • 学习了移动端WebRTC的安全配置最佳实践

移动端WebRTC实践

  • 解决了Android WebView中金融级应用的兼容性问题
  • 掌握了Crossover框架在敏感业务场景中的使用
  • 学习了移动端音视频处理的安全最佳实践

安全和合规

  • 理解了金融级应用的SSL/TLS安全要求
  • 掌握了敏感数据的加密传输和存储
  • 学习了区块链存证技术的应用

2. 项目价值

技术突破

  • 成功在Android WebView中实现了金融级的视频签约功能
  • 解决了移动端WebRTC在金融场景中的安全性和合规性问题
  • 建立了完整的视频签约技术栈和安全体系

业务价值

  • 为金融、法律、房地产等行业提供了安全可靠的在线签约解决方案
  • 大幅降低了传统签约的物理成本和时间成本
  • 提升了用户体验和业务处理效率

3. 最佳实践

// 视频签约最佳实践总结
object VideoSigningBestPractices {
    // 1. 安全要求
    const val MIN_SSL_VERSION = "TLSv1.2"
    const val MIN_VIDEO_QUALITY = "720p"
    const val MIN_AUDIO_QUALITY = "48kHz"
    const val MAX_SESSION_TIMEOUT = 30 * 60 * 1000 // 30分钟

    // 2. 性能要求
    const val MAX_CONNECTION_DELAY = 5000 // 5秒
    const val MAX_VIDEO_BITRATE = 1500 // kbps
    const val MAX_AUDIO_BITRATE = 256 // kbps

    // 3. 合规要求
    val REQUIRED_DOCUMENTS = setOf("身份证", "合同", "签名")
    val REQUIRED_VERIFICATIONS = setOf("人脸识别", "活体检测", "身份核验")
    val REQUIRED_EVIDENCES = setOf("视频录制", "区块链存证", "操作日志")

    // 4. 错误处理
    val CRITICAL_ERRORS = setOf(
        "SSL_CERTIFICATE_ERROR",
        "IDENTITY_VERIFICATION_FAILED",
        "CONNECTION_SECURITY_BREACH",
        "ELECTRONIC_SIGNATURE_FAILED"
    )
}

后续规划

  1. 技术升级:跟进WebRTC最新标准和金融安全规范
  2. 功能扩展:支持多签、批量签约、智能合同等高级功能
  3. 合规认证:通过等保三级、GDPR等安全合规认证
  4. AI集成:集成AI能力如智能风控、语音识别、文档识别等
  5. 区块链深化:深化区块链技术在电子存证中的应用

总结

2019年的WebRTC视频签约技术研究是一次非常有价值的技术探索。通过这次实践,我们不仅解决了Android WebView中WebRTC的兼容性问题,还深入理解了金融级应用的安全要求和技术实现。

技术突破

  • 成功实现了移动端金融级视频签约的完整功能
  • 解决了Crossover框架在金融场景中的SSL安全配置问题
  • 建立了完整的视频签约安全技术栈

经验积累

  • 掌握了WebRTC在金融级应用中的安全最佳实践
  • 学习了移动端音视频处理的安全合规要求
  • 积累了金融级应用开发的宝贵经验

这次WebRTC视频签约研究为后续的金融科技项目奠定了坚实的技术基础,也让我对安全合规的实时通信技术有了更深的认识。

WebRTC视频签约研究感悟

  • 金融级应用对安全性的要求远超普通应用
  • 技术实现必须兼顾功能性和合规性
  • 用户信任建立在技术安全的基础之上
  • 持续学习和适应安全标准的变化是必要的
-- 次访问
Powered by Hugo & Stack Theme
使用 Hugo 构建
主题 StackJimmy 设计