Видео не отображается при вызове WebRTC между iOS и iOS, несмотря на работающее шифрование и дешифрование

Нет видео на экране при звонке с iOS на iOS по зашифрованному каналу с WebRTC.

Есть приложение, которое работает на Android и iOS и имеет вызовы WebRTC. На Android голос и видео зашифрованы с помощью WebView с пользовательским кодом JS, а на iOS есть библиотека WebRTC от Google с дополнительной возможностью использования шифрования (с использованием FrameEncryptorInterface и FrameDecryptorInterface в качестве основы).

Ситуация:

  • Android с Android: шифрование и дешифрование работают нормально, а также реальное видео на экране и звук
  • Android с iOS работает нормально, как и реальное видео на экране и звук
  • iOS с iOS имеют работающее шифрование и расшифровку как для аудио, так и для видео, имеют рабочий звук, но не работает видео на экране. Показывает только пустой экран для входящего видео. Однако, как я уже сказал, шифрование и дешифрование работают нормально (логи вижу).

Сбоев нет, вроде все должно работать, но видео с iOS на iOS по-прежнему не показывается на экране.

Так как используется так много компонентов на разных языках (Typescript, Haskell, Swift), я привожу основную их часть:

Android (источник):

function callCryptoFunction(): CallCrypto {
  const initialPlainTextRequired = {
    key: 10,
    delta: 3,
    empty: 1,
  }

  const IV_LENGTH = 12

  function encryptFrame(key: CryptoKey): (frame: RTCEncodedVideoFrame, controller: TransformStreamDefaultController) => Promise<void> {
    return async (frame, controller) => {
      const data = new Uint8Array(frame.data)
      const n = initialPlainTextRequired[frame.type] || 1
      const iv = randomIV()
      const initial = data.subarray(0, n)
      const plaintext = data.subarray(n, data.byteLength)
      try {
        const ciphertext = plaintext.length
          ? new Uint8Array(await crypto.subtle.encrypt({name: "AES-GCM", iv: iv.buffer}, key, plaintext))
          : new Uint8Array(0)
        frame.data = concatN(initial, ciphertext, iv).buffer
        controller.enqueue(frame)
      } catch (e) {
        console.info(`encryption error ${e}`)
        throw e
      }
    }
  }

  function decryptFrame(key: CryptoKey): (frame: RTCEncodedVideoFrame, controller: TransformStreamDefaultController) => Promise<void> {
    return async (frame, controller) => {
      const data = new Uint8Array(frame.data)
      const n = initialPlainTextRequired[frame.type] || 1
      const initial = data.subarray(0, n)
      const ciphertext = data.subarray(n, data.byteLength - IV_LENGTH)
      const iv = data.subarray(data.byteLength - IV_LENGTH, data.byteLength)
      try {
        const plaintext = ciphertext.length
          ? new Uint8Array(await crypto.subtle.decrypt({name: "AES-GCM", iv}, key, ciphertext))
          : new Uint8Array(0)
        frame.data = concatN(initial, plaintext).buffer
        controller.enqueue(frame)
      } catch (e) {
        console.info(`decryption error ${e}`)
        throw e
      }
    }
  }

  function decodeAesKey(aesKey: string): Promise<CryptoKey> {
    const keyData = callCrypto.decodeBase64url(callCrypto.encodeAscii(aesKey))
    return crypto.subtle.importKey("raw", keyData!, {name: "AES-GCM", length: 256}, true, ["encrypt", "decrypt"])
  }

  function randomIV() {
    return crypto.getRandomValues(new Uint8Array(IV_LENGTH))
  }
//...

}

Дополнения встроенной библиотеки WebRTC для iOS (источник):

// ДЕКРИПТОР


#import <Foundation/Foundation.h>
#import "RTCFrameDecryptor+Private.h"
#import "base/RTCLogging.h"

#include "rtc_base/checks.h"
#include "api/crypto/frame_decryptor_interface.h"
#include "RTCRtpReceiver+Private.h"

namespace webrtc {
RTCFrameDecryptorAdapter::RTCFrameDecryptorAdapter(RTC_OBJC_TYPE(RTCFrameDecryptor) * decryptor, size_t sizeChange) {
  RTC_CHECK(decryptor);
  decryptor_ = decryptor;
  sizeChange_ = sizeChange;
}

RTCFrameDecryptorAdapter::Result RTCFrameDecryptorAdapter::Decrypt(
    cricket::MediaType media_type,
    const std::vector<uint32_t>& csrcs,
    rtc::ArrayView<const uint8_t> additional_data,
    rtc::ArrayView<const uint8_t> encrypted_frame,
    rtc::ArrayView<uint8_t> frame) {
  RTC_OBJC_TYPE(RTCFrameDecryptor) *decryptor = decryptor_;
  NSData *encrypted = [NSData dataWithBytes: encrypted_frame.data() length: encrypted_frame.size()];
  NSData *decrypted = [decryptor.delegate frameDecryptor:decryptor mediaType:[RTC_OBJC_TYPE(RTCRtpReceiver) mediaTypeForNativeMediaType: media_type] withFrame:encrypted];
  if (decrypted) {
    const char * decryptedBytes = (const char *)[decrypted bytes];
    NSUInteger size = [decrypted length];
    for (size_t i = 0; i < size; i++) {
      frame[i] = decryptedBytes[i];
    }
    return Result(Status::kOk, size);
  } else {
    return Result(Status::kFailedToDecrypt, 0);
  }

}

size_t RTCFrameDecryptorAdapter::GetMaxPlaintextByteSize(
  cricket::MediaType media_type,
  size_t encrypted_frame_size) {
  return encrypted_frame_size + sizeChange_;
}
}  // namespace webrtc


@implementation RTC_OBJC_TYPE (RTCFrameDecryptor) {
  rtc::scoped_refptr<webrtc::FrameDecryptorInterface> _nativeFrameDecryptor;
}

@synthesize delegate = _delegate;
@synthesize nativeFrameDecryptor = _nativeFrameDecryptor;

- (NSString *)description {
  return [NSString
      stringWithFormat:@"RTC_OBJC_TYPE(RTCFrameDecryptor)"];
}

- (void)dealloc {
  _nativeFrameDecryptor = nullptr;
}

- (BOOL)isEqual:(id)object {
  if (self == object) {
    return YES;
  }
  if (object == nil) {
    return NO;
  }
  if (![object isMemberOfClass:[self class]]) {
    return NO;
  }
  RTC_OBJC_TYPE(RTCFrameDecryptor) *decryptor = (RTC_OBJC_TYPE(RTCFrameDecryptor) *)object;
  return _nativeFrameDecryptor == decryptor.nativeFrameDecryptor;
}

- (NSUInteger)hash {
  return (NSUInteger)_nativeFrameDecryptor.get();
}

- (instancetype)initWithSizeChange:(int)sizeChange {
  if (self = [super init]) {
    RTCLogInfo(@"RTC_OBJC_TYPE(RTCFrameDecryptor)(%p): created decryptor: %@", self, self.description);
    _nativeFrameDecryptor = new webrtc::RTCFrameDecryptorAdapter(self, sizeChange);
  }
  return self;
}

@end

// ШИФРОВАТЕЛЬ

#import <Foundation/Foundation.h>
#import "RTCFrameEncryptor+Private.h"
#import "base/RTCLogging.h"

#include "rtc_base/checks.h"
#include "api/crypto/frame_encryptor_interface.h"
#include "RTCRtpReceiver+Private.h"

namespace webrtc {
RTCFrameEncryptorAdapter::RTCFrameEncryptorAdapter(RTC_OBJC_TYPE(RTCFrameEncryptor) * encryptor, size_t sizeChange) {
  RTC_CHECK(encryptor);
  encryptor_ = encryptor;
  sizeChange_ = sizeChange;
}

int RTCFrameEncryptorAdapter::Encrypt(
    cricket::MediaType media_type,
    uint32_t ssrc,
    rtc::ArrayView<const uint8_t> additional_data,
    rtc::ArrayView<const uint8_t> frame,
    rtc::ArrayView<uint8_t> encrypted_frame,
    size_t* bytes_written) {
  RTC_OBJC_TYPE(RTCFrameEncryptor) *encryptor = encryptor_;
  NSData *unencrypted = [NSData dataWithBytes: frame.data() length: frame.size()];
  NSData *encrypted = [encryptor.delegate frameEncryptor:encryptor mediaType:[RTC_OBJC_TYPE(RTCRtpReceiver) mediaTypeForNativeMediaType: media_type] withFrame:unencrypted];
  if (encrypted) {
    const char *encryptedBytes = (const char *)[encrypted bytes];
    NSUInteger size = [encrypted length];
    for (size_t i = 0; i < size; i++) {
      encrypted_frame[i] = encryptedBytes[i];
    }
    *bytes_written = size;
    return 0;
  } else {
    return 1;
  }

}

size_t RTCFrameEncryptorAdapter::GetMaxCiphertextByteSize(
  cricket::MediaType media_type,
  size_t frame_size) {
  return frame_size + sizeChange_;
}
}  // namespace webrtc


@implementation RTC_OBJC_TYPE (RTCFrameEncryptor) {
  rtc::scoped_refptr<webrtc::FrameEncryptorInterface> _nativeFrameEncryptor;
}

@synthesize delegate = _delegate;
@synthesize nativeFrameEncryptor = _nativeFrameEncryptor;

- (NSString *)description {
  return [NSString
      stringWithFormat:@"RTC_OBJC_TYPE(RTCFrameEncryptor)"];
}

- (void)dealloc {
  _nativeFrameEncryptor = nullptr;
}

- (BOOL)isEqual:(id)object {
  if (self == object) {
    return YES;
  }
  if (object == nil) {
    return NO;
  }
  if (![object isMemberOfClass:[self class]]) {
    return NO;
  }
  RTC_OBJC_TYPE(RTCFrameEncryptor) *encryptor = (RTC_OBJC_TYPE(RTCFrameEncryptor) *)object;
  return _nativeFrameEncryptor == encryptor.nativeFrameEncryptor;
}

- (NSUInteger)hash {
  return (NSUInteger)_nativeFrameEncryptor.get();
}

- (instancetype)initWithSizeChange:(int)sizeChange {
  if (self = [super init]) {
    RTCLogInfo(@"RTC_OBJC_TYPE(RTCFrameEncryptor)(%p): created encryptor: %@", self, self.description);
    _nativeFrameEncryptor = new webrtc::RTCFrameEncryptorAdapter(self, sizeChange);
  }
  return self;
}

@end

iOS Swift (источник):

// WebRTCClient.swift

private static let ivTagBytes: Int = 28 // iv + tag, 12 + 16

let encryptor = RTCFrameEncryptor.init(sizeChange: Int32(WebRTCClient.ivTagBytes))
let decryptor = RTCFrameDecryptor.init(sizeChange: -Int32(WebRTCClient.ivTagBytes))

    func frameDecryptor(_ decryptor: RTCFrameDecryptor, mediaType: RTCRtpMediaType, withFrame encrypted: Data) -> Data? {
        guard encrypted.count > 0 else { return nil }
        if var key: [CChar] = activeCall.wrappedValue?.aesKey?.cString(using: .utf8),
           let pointer: UnsafeMutableRawPointer = malloc(encrypted.count) {
            memcpy(pointer, (encrypted as NSData).bytes, encrypted.count)
            let isKeyFrame = encrypted[0] & 1 == 0
            let clearTextBytesSize = mediaType.rawValue == 0 ? 1 : isKeyFrame ? 10 : 3
            logCrypto("decrypt", chat_decrypt_media(&key, pointer.advanced(by: clearTextBytesSize), Int32(encrypted.count - clearTextBytesSize)))
            return Data(bytes: pointer, count: encrypted.count - WebRTCClient.ivTagBytes)
        } else {
            return nil
        }
    }

    func frameEncryptor(_ encryptor: RTCFrameEncryptor, mediaType: RTCRtpMediaType, withFrame unencrypted: Data) -> Data? {
        guard unencrypted.count > 0 else { return nil }
        if var key: [CChar] = activeCall.wrappedValue?.aesKey?.cString(using: .utf8),
           let pointer: UnsafeMutableRawPointer = malloc(unencrypted.count + WebRTCClient.ivTagBytes) {
            memcpy(pointer, (unencrypted as NSData).bytes, unencrypted.count)
            let isKeyFrame = unencrypted[0] & 1 == 0
            let clearTextBytesSize = mediaType.rawValue == 0 ? 1 : isKeyFrame ? 10 : 3
            logCrypto("encrypt", chat_encrypt_media(&key, pointer.advanced(by: clearTextBytesSize), Int32(unencrypted.count + WebRTCClient.ivTagBytes - clearTextBytesSize)))
            return Data(bytes: pointer, count: unencrypted.count + WebRTCClient.ivTagBytes)
        } else {
            return nil
        }
    }


Часть шифрования/дешифрования iOS (источник):

chatEncryptMedia :: ByteString -> ByteString -> ExceptT String IO ByteString
chatEncryptMedia keyStr frame = do
  len <- checkFrameLen frame
  key <- decodeKey keyStr
  iv <- liftIO C.randomGCMIV
  (tag, frame') <- withExceptT show $ C.encryptAESNoPad key iv $ B.take len frame
  pure $ frame' <> BA.convert (C.unAuthTag tag) <> C.unGCMIV iv

chatDecryptMedia :: ByteString -> ByteString -> ExceptT String IO ByteString
chatDecryptMedia keyStr frame = do
  len <- checkFrameLen frame
  key <- decodeKey keyStr
  let (frame', rest) = B.splitAt len frame
      (tag, iv) = B.splitAt C.authTagSize rest
      authTag = C.AuthTag $ AES.AuthTag $ BA.convert tag
  withExceptT show $ do
    iv' <- liftEither $ C.gcmIV iv
    frame'' <- C.decryptAESNoPad key iv' frame' authTag
    pure $ frame'' <> framePad

checkFrameLen :: ByteString -> ExceptT String IO Int
checkFrameLen frame = do
  let len = B.length frame - reservedSize
  when (len < 0) $ throwError "frame has no [reserved space for] IV and/or auth tag"
  pure len
{-# INLINE checkFrameLen #-}

decodeKey :: ByteString -> ExceptT String IO C.Key
decodeKey = liftEither . bimap ("invalid key: " <>) C.Key . U.decode
{-# INLINE decodeKey #-}

reservedSize :: Int
reservedSize = C.authTagSize + C.gcmIVSize

framePad :: ByteString
framePad = B.replicate reservedSize 0

Пожалуйста, нажмите на ссылку «исходный код», чтобы просмотреть весь код, если вам нужно больше контекста.

У вас есть идея, почему нет видео на экране при вызове iOS-to-iOS?

Я пытался:

  • чтобы поместить шифровальщик и расшифровщик, присоединяющий действие к конкретному отправителю и получателю, в разные состояния соединений WebRTC (например, подключено, соединение, проверка и т. д.)
  • изменить параметр sizeChange, но приложение вылетает или после этого ничего не делает (зависит от увеличения или уменьшения значения)
  • сравнить зашифрованный и расшифрованный массив байтов на обоих концах на Android-iOS и iOS-iOS, и результат был таким же. Я имею в виду, что результат был таким, каким он должен быть
  • комментировать строки
logCrypto("decrypt", chat_decrypt_media(&key, pointer.advanced(by: clearTextBytesSize), Int32(encrypted.count - clearTextBytesSize)))

и

logCrypto("encrypt", chat_encrypt_media(&key, pointer.advanced(by: clearTextBytesSize), Int32(unencrypted.count + WebRTCClient.ivTagBytes - clearTextBytesSize)))

После этого все работает нормально, но шифрования в данном случае, естественно, нет.


1
73
1

Ответ:

Решено

Ответом была только эта одна строка. По какой-то причине, даже при звонках между iOS-устройствами, мне приходится указывать в RTCDefaultVideoDecoderFactory PreferredCodec. Так:

private static let factory: RTCPeerConnectionFactory = {
        RTCInitializeSSL()
        let videoEncoderFactory = RTCDefaultVideoEncoderFactory()
        let videoDecoderFactory = RTCDefaultVideoDecoderFactory()
+        videoEncoderFactory.preferredCodec = RTCVideoCodecInfo(name: kRTCVp8CodecName)
        return RTCPeerConnectionFactory(encoderFactory: videoEncoderFactory, decoderFactory: videoDecoderFactory)
    }()

Теперь все работает нормально.