iOSReplayKit插件使用

  • iOSReplayKit用途
  • 开始使用
  • 修改代码
  • 增加MIC录制
  • 完整代码
  • Objective-C和C++混合编码

iOSReplayKit用途


ReplayKit是苹果为iOS/tvOS/macOS平台视频直播和视频录制提供的工具包(Record or stream video from the screen, and audio from the app and microphone.),整个插件使用很简单(因为大部分功能都被苹果限制死了),调用StartXxxxWithHandler开始录制,提供回调函数接收调用结果和数据包;StopXxxxWithHandler结束录制,提供回调函数接收调用结果。

iOSReplayKit(ReplayKit for iOS)插件是提供给UE使用iOS平台ReplayKit软件包的桥接器。插件包含一个蓝图类UIOSReplayKitControl,提供调用iOS ReplayKit API函数的蓝图接口;一个Objective-C语言写的ReplayKitRecorder实现类。

开始使用

因为要在Unreal开发的iOS平台APP使用视频录制功能,查找资料发现苹果提供的ReplayKit可以支持视频和麦克风录制。进一步查找Unreal平台插件,在github上找到一个replaykit for ios的项目
PushkinStudio/PsReplayKit
但这个项目最后更新已经是2018年了。
后来查看Unreal引擎源码,发现Unreal引擎里面已经集成ReplayKit插件。
直接在项目引入插件,重启
插件使用很简单,一共提供了四个函数

Unreal本着已经提供了源代码没必要再提供文档的原则,查看帮助文档,得到的信息也不会比函数名更多了。

至于StartRecording和StartCaptureToFile有什么区别,查看源代码,发现确实是Apple的锅,Apple ReplayKit提供startRecordingWithHandler和startCaptureWithHandler两个方法,startCaptureWithHandler提供了回调函数,可以自己处理录制过程;startRecordingWithHandler不提供过程回调,仅在stop时提供预览(调用系统预览框),全程开发者无法干预。

创建两个按钮,点击绑定调用StartCaptureToFile和StopCapture,打包输出,在真机上运行点击Start录制,成功唤起权限确认对话框。

点击Stop结束,显示存储到相册权限提示,看起来一切正常。
在测试几次,结果发现问题了,除了一开始成功的一次,后面基本上都是失败的,并没有任何文件存储到相册。
然后换了StartRecording和StopRecording接口,一样的问题,偶尔成功,大部分失败。
查看源代码,发现StartCaptureToFile调用startCapture,该函数实现代码是自己将回调数据包写入mp4文件,其中有处理文件的代码

  // create the asset writer[_assetWriter release] ;// todo: do we care about the file name? support cleaning up old captures?auto fileName = FString::Printf(TEXT("%s.mp4"), *FGuid::NewGuid().ToString());_captureFilePath = [captureDir stringByAppendingFormat : @"/%@", fileName.GetNSString()];[_captureFilePath retain] ;_assetWriter = [[AVAssetWriter alloc]initWithURL: [NSURL fileURLWithPath : _captureFilePath] fileType : AVFileTypeMPEG4 error : nil];// _assetWriter = [AVAssetWriter assetWriterWithURL : [NSURL fileURLWithPath : _captureFilePath] fileType : AVFileTypeMPEG4 error : nil];[_assetWriter retain] ;

通过USB连接iPAD,在文件浏览里面发现Capture目录下大部分录制的mp4都是0字节文件,说明在写入环节出现问题了

查看写入代码,增加Unreal屏幕打印调式信息功能

        if (_assetWriter.status == AVAssetWriterStatusUnknown){[_assetWriter startWriting] ;[_assetWriter startSessionAtSourceTime : CMSampleBufferGetPresentationTimeStamp(sampleBuffer)] ;if (GEngine) {CMTime time = CMSampleBufferGetPresentationTimeStamp(sampleBuffer);GEngine->AddOnScreenDebugMessage(-1, 5.f, FColor::Blue, FString::Printf(TEXT("_assetWriter startWriting: %d"), (int)CMTimeGetSeconds(time)));}}if (_assetWriter.status == AVAssetWriterStatusFailed){NSLog(@"%d: %@", (int)_assetWriter.error.code, _assetWriter.error.localizedDescription);if (GEngine) {const char* dest = [_assetWriter.error.localizedDescription UTF8String];GEngine->AddOnScreenDebugMessage(-1, 5.f, FColor::Green, FString::Printf(TEXT("_assetWriter failed: %d %s"), (int)_assetWriter.error.code, UTF8_TO_TCHAR(dest)));}}

运行后屏幕打印的调试信息表明回调函数没有问题,问题出在AVAssetWriter状态是AVAssetWriterStatusFailed,错误代码为-11823,错误描述cannot save。
经历漫长编译-输出-安装-调试,发现蓝色提示信息_assetWriter startWriting 16600

GEngine->AddOnScreenDebugMessage(-1, 5.f, FColor::Blue, FString::Printf(TEXT("_assetWriter startWriting: %d"), (int)CMTimeGetSeconds(time)));

夹杂在绿色-11823,cannot save错误信息之间显示了两条。蓝色提示信息是在调用_assetWriter.startWriting时显示的,显然调用了两次startWriting导致AV视频写入器报错了。

        if (_assetWriter.status == AVAssetWriterStatusUnknown){[_assetWriter startWriting] ;// ...}

代码在调用startWriting时判断了写入器状态是不是无状态,调用startWriting以后写入器的状态应该会发生变化,线性调用是不会导致调用两次的,除非是多线程同时调用。连续调用了两次startWriting说明在状态改变之前又有新的回调函数调用了,猜想视频数据包和音频数据包处理是在不同线程进行的,几乎同时调用了回调函数,而回调函数并没有采用任何的线程锁机制,导致startWriting被调用了两次,从而引起AVAssetWriterStatusFailed错误,而偶尔成功的几次,应该是多线程没有同时回调,侥幸成功。

修改代码

增加一个frames自增变量,仅在0==frames时调用startWriting函数

        if (0 == _frames++) {[_assetWriter startWriting] ;[_assetWriter startSessionAtSourceTime : CMSampleBufferGetPresentationTimeStamp(sampleBuffer)] ;if (GEngine) {CMTime time = CMSampleBufferGetPresentationTimeStamp(sampleBuffer);GEngine->AddOnScreenDebugMessage(-1, 5.f, FColor::Blue, FString::Printf(TEXT("_assetWriter startWriting: %d"), (int)CMTimeGetSeconds(time)));}}

这里没有用线程锁,用了++自增操作,++操作仅一个CPU指令周期,几乎不可能被中断。

增加MIC录制

          AVAssetWriterInput* input = nullptr;// ...else if (bufferType == RPSampleBufferTypeAudioMic){// todo?}// ...

bufferType == RPSampleBufferTypeAudioMic表示这是一个Microphone采集回调数据,但代码中仅有一个todo?注释,并没有任何代码实现,也就是直接使用插件是不能录制麦克风声音的。(直到5.1版本的iOSReplayKit都没有实现Mic采集)
仿照RPSampleBufferTypeAudioApp内录音频,创建一个Mic录制写入器

  // create the audio input[_audioInput release] ;AudioChannelLayout acl;bzero(&acl, sizeof(acl));acl.mChannelLayoutTag = kAudioChannelLayoutTag_Stereo;auto audioSettings = @{AVFormatIDKey: @(kAudioFormatMPEG4AAC),AVSampleRateKey: @(44100),AVChannelLayoutKey: [NSData dataWithBytes : &acl length : sizeof(acl)] ,};_audioInput = [AVAssetWriterInput assetWriterInputWithMediaType : AVMediaTypeAudio outputSettings : audioSettings];_audioInput.expectsMediaDataInRealTime = YES;[_audioInput retain] ;// create the microphone input[_microInput release] ;AudioChannelLayout mic;bzero(&mic, sizeof(mic));mic.mChannelLayoutTag = kAudioChannelLayoutTag_Mono;auto micSettings = @{AVFormatIDKey: @(kAudioFormatMPEG4AAC),AVSampleRateKey: @(44100),AVChannelLayoutKey: [NSData dataWithBytes : &mic length : sizeof(mic)] ,};_microInput = [AVAssetWriterInput assetWriterInputWithMediaType : AVMediaTypeAudio outputSettings : micSettings];_microInput.expectsMediaDataInRealTime = YES;[_microInput retain] ;// add the input to the writer// ...if ([_assetWriter canAddInput : _audioInput]){[_assetWriter addInput : _audioInput] ;}if ([_assetWriter canAddInput : _microInput]){[_assetWriter addInput : _microInput] ;}

todo?注释增加mic input输入处理

          AVAssetWriterInput* input = nullptr;if (bufferType == RPSampleBufferTypeVideo){input = _videoInput;}else if (bufferType == RPSampleBufferTypeAudioApp){input = _audioInput;}else if (bufferType == RPSampleBufferTypeAudioMic){// todo?input = _microInput;}if (input && input.isReadyForMoreMediaData){[input appendSampleBuffer : sampleBuffer] ;}

增加上面代码后,启动startCapture时勾选麦克风,就可以录制麦克风声音了(不知道官方插件为啥不实现这个简单的功能)

完整代码

// Copyright Epic Games, Inc. All Rights Reserved.#include "ReplayKitRecorder.h"#if PLATFORM_IOS
#include "IOSAppDelegate.h"
#include "IOS/IOSView.h"
#include "Misc/Paths.h"
#include "Engine/Engine.h"
#include "Engine/GameViewportClient.h"@implementation ReplayKitRecorderRPScreenRecorder* _Nullable _screenRecorder;
RPBroadcastActivityViewController* _Nullable _broadcastActivityController;
RPBroadcastController* _Nullable _broadcastController;// stuff used when capturing to file
AVAssetWriter* _Nullable _assetWriter;
AVAssetWriterInput* _Nullable _videoInput;
AVAssetWriterInput* _Nullable _audioInput;
AVAssetWriterInput* _Nullable _microInput;
NSString* _Nullable _captureFilePath;
int _frames;- (void) initializeWithMicrophoneEnabled:(BOOL)bMicrophoneEnabled withCameraEnabled:(BOOL)bCameraEnabled {_screenRecorder = [RPScreenRecorder sharedRecorder];[_screenRecorder setDelegate:self];
#if !PLATFORM_TVOS[_screenRecorder setMicrophoneEnabled:bMicrophoneEnabled];[_screenRecorder setCameraEnabled:bCameraEnabled];
#endif
}- (void)startRecording {// NOTE(omar): stop any live broadcasts before staring a local recordingif( _broadcastController != nil ) {[self stopBroadcast];}if( [_screenRecorder isAvailable] ) {[_screenRecorder startRecordingWithHandler:^(NSError * _Nullable error) {if( error ) {NSLog( @"error starting screen recording");}}];}
}- (void)stopRecording {if( [_screenRecorder isAvailable] && [_screenRecorder isRecording] ) {[_screenRecorder stopRecordingWithHandler:^(RPPreviewViewController * _Nullable previewViewController, NSError * _Nullable error) {[previewViewController setPreviewControllerDelegate:self];// automatically show the video preview when recording is stoppedpreviewViewController.popoverPresentationController.sourceView = (UIView* _Nullable)[IOSAppDelegate GetDelegate].IOSView;[[IOSAppDelegate GetDelegate].IOSController presentViewController:previewViewController animated:YES completion:nil];}];}
}- (void)createCaptureContext
{auto fileManager = [NSFileManager defaultManager];auto docDir = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) objectAtIndex:0];auto captureDir = [docDir stringByAppendingFormat : @"/Captures"];[fileManager createDirectoryAtPath : captureDir withIntermediateDirectories : YES attributes : nil error : nil] ;[_captureFilePath release] ;// create the asset writer[_assetWriter release] ;// todo: do we care about the file name? support cleaning up old captures?auto fileName = FString::Printf(TEXT("%s.mp4"), *FGuid::NewGuid().ToString());_captureFilePath = [captureDir stringByAppendingFormat : @"/%@", fileName.GetNSString()];[_captureFilePath retain] ;_assetWriter = [[AVAssetWriter alloc]initWithURL: [NSURL fileURLWithPath : _captureFilePath] fileType : AVFileTypeMPEG4 error : nil];// _assetWriter = [AVAssetWriter assetWriterWithURL : [NSURL fileURLWithPath : _captureFilePath] fileType : AVFileTypeMPEG4 error : nil];[_assetWriter retain] ;// create the video input[_videoInput release] ;auto view = [IOSAppDelegate GetDelegate].IOSView;auto width = [NSNumber numberWithFloat : view.frame.size.width];auto height = [NSNumber numberWithFloat : view.frame.size.height];if (GEngine && GEngine->GameViewport && GEngine->GameViewport->Viewport){auto viewportSize = GEngine->GameViewport->Viewport->GetSizeXY();width = [NSNumber numberWithInt : viewportSize.X];height = [NSNumber numberWithInt : viewportSize.Y];}auto videoSettings = @{AVVideoCodecKey: AVVideoCodecTypeH264,AVVideoWidthKey : width,AVVideoHeightKey : height};_videoInput = [AVAssetWriterInput assetWriterInputWithMediaType : AVMediaTypeVideo outputSettings : videoSettings];_videoInput.expectsMediaDataInRealTime = YES;[_videoInput retain] ;// create the audio input[_audioInput release] ;AudioChannelLayout acl;bzero(&acl, sizeof(acl));acl.mChannelLayoutTag = kAudioChannelLayoutTag_Stereo;auto audioSettings = @{AVFormatIDKey: @(kAudioFormatMPEG4AAC),AVSampleRateKey: @(44100),AVChannelLayoutKey: [NSData dataWithBytes : &acl length : sizeof(acl)] ,};_audioInput = [AVAssetWriterInput assetWriterInputWithMediaType : AVMediaTypeAudio outputSettings : audioSettings];_audioInput.expectsMediaDataInRealTime = YES;[_audioInput retain] ;// create the microphone input[_microInput release] ;AudioChannelLayout mic;bzero(&mic, sizeof(mic));mic.mChannelLayoutTag = kAudioChannelLayoutTag_Mono;auto micSettings = @{AVFormatIDKey: @(kAudioFormatMPEG4AAC),AVSampleRateKey: @(44100),AVChannelLayoutKey: [NSData dataWithBytes : &mic length : sizeof(mic)] ,};_microInput = [AVAssetWriterInput assetWriterInputWithMediaType : AVMediaTypeAudio outputSettings : micSettings];_microInput.expectsMediaDataInRealTime = YES;[_microInput retain] ;// add the input to the writerif ([_assetWriter canAddInput : _videoInput]){[_assetWriter addInput : _videoInput] ;}if ([_assetWriter canAddInput : _audioInput]){[_assetWriter addInput : _audioInput] ;}if ([_assetWriter canAddInput : _microInput]){[_assetWriter addInput : _microInput] ;}
}- (void)startCapture
{if (_broadcastController){[self stopBroadcast] ;}if ([_screenRecorder isAvailable]){_frames = 0;[self createCaptureContext] ;[_screenRecorder startCaptureWithHandler : ^ (CMSampleBufferRef sampleBuffer, RPSampleBufferType bufferType, NSError * error){if (CMSampleBufferDataIsReady(sampleBuffer)){if (0 == _frames++) {[_assetWriter startWriting] ;[_assetWriter startSessionAtSourceTime : CMSampleBufferGetPresentationTimeStamp(sampleBuffer)] ;if (GEngine) {CMTime time = CMSampleBufferGetPresentationTimeStamp(sampleBuffer);GEngine->AddOnScreenDebugMessage(-1, 5.f, FColor::Blue, FString::Printf(TEXT("_assetWriter startWriting: %d"), (int)CMTimeGetSeconds(time)));}}/*if (_assetWriter.status == AVAssetWriterStatusUnknown){[_assetWriter startWriting] ;[_assetWriter startSessionAtSourceTime : CMSampleBufferGetPresentationTimeStamp(sampleBuffer)] ;if (GEngine) {CMTime time = CMSampleBufferGetPresentationTimeStamp(sampleBuffer);GEngine->AddOnScreenDebugMessage(-1, 5.f, FColor::Blue, FString::Printf(TEXT("_assetWriter startWriting: %d"), (int)CMTimeGetSeconds(time)));}}*/if (_assetWriter.status == AVAssetWriterStatusFailed){// NSLog(@"%d: %@", (int)_assetWriter.error.code, _assetWriter.error.localizedDescription);if (GEngine) {const char* dest = [_assetWriter.error.localizedDescription UTF8String];GEngine->AddOnScreenDebugMessage(-1, 5.f, FColor::Green, FString::Printf(TEXT("_assetWriter failed: %d %s"), (int)_assetWriter.error.code, UTF8_TO_TCHAR(dest)));}}else {AVAssetWriterInput* input = nullptr;if (bufferType == RPSampleBufferTypeVideo){input = _videoInput;}else if (bufferType == RPSampleBufferTypeAudioApp){input = _audioInput;}else if (bufferType == RPSampleBufferTypeAudioMic){// todo?input = _microInput;}if (input && input.isReadyForMoreMediaData){[input appendSampleBuffer : sampleBuffer] ;}}}}completionHandler: ^ (NSError * error){if (error){NSLog(@"completionHandler: %@", error);if (GEngine) {GEngine->AddOnScreenDebugMessage(-1, 5.f, FColor::Red, FString::Printf(TEXT("error at startCapture")));}}}];}
}- (void)stopCapture
{if ([_screenRecorder isAvailable]){[_screenRecorder stopCaptureWithHandler:^(NSError *error){if (error){NSLog(@"stopCaptureWithHandler: %@", error);}if (_assetWriter){[_assetWriter finishWritingWithCompletionHandler:^(){NSLog(@"finishWritingWithCompletionHandler");#if !PLATFORM_TVOSif (UIVideoAtPathIsCompatibleWithSavedPhotosAlbum(_captureFilePath)){UISaveVideoAtPathToSavedPhotosAlbum(_captureFilePath, nil, nil, nil);NSLog(@"capture saved to album");}
#endif[_captureFilePath release];_captureFilePath = nullptr;[_videoInput release];_videoInput = nullptr;[_audioInput release];_audioInput = nullptr;[_microInput release];_microInput = nullptr;[_assetWriter release];_assetWriter = nullptr;}];}}];}
}//
// livestreaming functionality
//- (void)startBroadcast {// NOTE(omar): ending any local recordings that might be active before starting a broadcastif( [_screenRecorder isRecording] ) {[self stopRecording];}[RPBroadcastActivityViewController loadBroadcastActivityViewControllerWithHandler:^(RPBroadcastActivityViewController * _Nullable broadcastActivityViewController, NSError * _Nullable error) {_broadcastActivityController = broadcastActivityViewController;[_broadcastActivityController setDelegate:self];[[IOSAppDelegate GetDelegate].IOSController presentViewController:_broadcastActivityController animated:YES completion:nil];}];
}- (void)pauseBroadcast {if( [_broadcastController isBroadcasting] ) {[_broadcastController pauseBroadcast];}
}- (void)resumeBroadcast {if( [_broadcastController isPaused] ) {[_broadcastController resumeBroadcast];}
}- (void)stopBroadcast {if( [_broadcastController isBroadcasting ] ) {[_broadcastController finishBroadcastWithHandler:^(NSError * _Nullable error) {if( error ) {NSLog( @"error finishing broadcast" );}[_broadcastController release];_broadcastController = nil;}];}
}//
// delegates
//// screen recorder delegate
- (void)screenRecorder:(RPScreenRecorder* _Nullable)screenRecorder didStopRecordingWithError:(NSError* _Nullable)error previewViewController:(RPPreviewViewController* _Nullable)previewViewController {NSLog(@"RTRScreenRecorderDelegate::didStopRecrodingWithError");[previewViewController dismissViewControllerAnimated:YES completion:nil];
}- (void)screenRecorderDidChangeAvailability:(RPScreenRecorder* _Nullable)screenRecorder {NSLog(@"RTRScreenRecorderDelegate::screenRecorderDidChangeAvailability");
}// screen recorder preview view controller delegate
- (void)previewControllerDidFinish:(RPPreviewViewController* _Nullable)previewController {NSLog( @"RTRPreviewViewControllerDelegate::previewControllerDidFinish" );[previewController dismissViewControllerAnimated:YES completion:nil];
}- (void)previewController:(RPPreviewViewController* _Nullable)previewController didFinishWithActivityTypes:(NSSet <NSString*> * _Nullable)activityTypes __TVOS_PROHIBITED {NSLog( @"RTRPreviewViewControllerDelegate::didFinishWithActivityTypes" );[previewController dismissViewControllerAnimated:YES completion:nil];
}// broadcast activity view controller delegate
- (void)broadcastActivityViewController:(RPBroadcastActivityViewController* _Nullable)broadcastActivityViewController didFinishWithBroadcastController:(RPBroadcastController* _Nullable)broadcastController error:(NSError* _Nullable)error {NSLog( @"RPBroadcastActivityViewControllerDelegate::didFinishWithBroadcastController" );[broadcastActivityViewController dismissViewControllerAnimated:YES completion:^{_broadcastController = [broadcastController retain];[_broadcastController setDelegate:self];[_broadcastController startBroadcastWithHandler:^(NSError* _Nullable _error) {if( _error ) {NSLog( @"error starting broadcast" );}}];}];
}// broadcast controller delegate
- (void)broadcastController:(RPBroadcastController* _Nullable)broadcastController didFinishWithError:(NSError* _Nullable)error {NSLog( @"RPBroadcastControllerDelegate::didFinishWithError" );
}- (void)broadcastController:(RPBroadcastController* _Nullable)broadcastController didUpdateServiceInfo:(NSDictionary <NSString*, NSObject <NSCoding>*> *_Nullable)serviceInfo {NSLog( @"RPBroadcastControllerDelegate::didUpdateServiceInfo" );
}@end#endif

Objective-C和C++混合编码

Objective-C和C++虽然都是C语言,但他们语法差异还是很大的。Objective-C和C++混合编程很简单,将Objective-C代码放在.h和.cpp文件中,就可以直接写Objective-C代码,也可以直接写C++代码,不需要做任何特殊处理,编译器可以直接编译混合了Objective-C代码和C++代码的.h文件和.cpp文件。

UE ReplayKit for iOS插件使用相关推荐

  1. 使用podspec创建iOS插件

    概述 在WWDC 2014全球开发者大会上,苹果开放了动态库.App Extension等全新的功能,这为iOS插件化开发带来了可能.在iOS开发中,动态库是iOS提供的一种资源打包方式,可以将代码文 ...

  2. 微信摇一摇插件ios_微信密友插件ios下载-微信密友ios插件下载6.6.6最新版-西西软件下载...

    微信密友ios插件是一款功能强大的苹果版微信密友隐藏软件,该插件支持微信密友隐藏.后台消息推送.群红包自动抢.运动步数修改等功能,功能强大,界面清爽,欢迎下载体验! 微信密友ios插件介绍: 1.Cy ...

  3. ios最新防越狱检测插件_-一份从零开始的iOS插件分享-

    最近我分享了很多关于越狱的插件,在这个文里我会一步一步告诉大家我是如何实现的. 在前面各位需要了解的是,这一切都建立在越狱之上,得益于P大以及众多大佬的努力,目前所有的iOS设备都可以越狱,并且使用极 ...

  4. iOS插件化研究之一——JavaScriptCore

    原文:http://chentoo.com/?p=191 一.前言 一样的开篇问题,为什么要研究这个?iOS为什么要插件化?为什么要借助其他语言比如html5 js甚至脚本lua等来实现原本OC/Sw ...

  5. 【虚幻引擎UE】UE5 超实用插件推荐

    一.MooaToon (UE5影视级卡通渲染的终极解决方案) MooaToon是一个旨在彻底解决 UE5 三渲二 痛点的插件,结合了UE原生的光照特性和强大的材质系统,释放美术师的潜力. 官网地址:h ...

  6. python开发ios插件_[原创]Textobot-换个轻松高效的方式开发iOS越狱插件

    许愿:要是段老师的看雪平台能导入头条或者公众号的文章就好了. 导读 0x00.交个朋友 0x01.越狱开发 0x02.Cydia插件体系 0x03.Cydia插件开发 0x04.Textobot插件体 ...

  7. iOS插件化架构探索

    +前言 WWDC2014苹果在iOS上开放了动态库,这给了我们一个很大的想象空间. 动态库即动态链接库,是Cocoa/Cocoa Touch程序中使用的一种资源打包方式,可以将代码文件.头文件.资源文 ...

  8. 优酷iOS插件化页面架构方法

    Python实战社群 Java实战社群 长按识别下方二维码,按需求添加 扫码关注添加客服 进Python社群▲ 扫码关注添加客服 进Java社群▲ 作者 | iOS一叶  来源 | 掘金,点击阅读原文 ...

  9. 优酷 iOS 插件化页面架构方法

    作者 | iOS一叶  来源 | 掘金,点击阅读原文查看作者更多文章 一.前言 随着业务不停地迭代,优酷 APP 用于分发视频资源的 UI 控件越写越多,也越来越复杂,并且同时相似相近的代码也非常多. ...

最新文章

  1. TCP 客户端程序开发
  2. Python自定义主从分布式架构
  3. Navicat 12连接MySQL8服务器
  4. 用Python建立最简单的web服务器
  5. 全球与中国节能冷却塔销售渠道分布及市场营销状况分析报告2022-2028年版
  6. java 定时_Java线上定时任务不定期挂掉问题分析
  7. java 读取 image_如何在java读取sql里头读取image格式的数据转换成图片格式
  8. python编码规范简单总结
  9. spark线性svm支持向量机 小结
  10. 实例:评审速度与缺陷密度之间的相关性
  11. ISO8583报文格式分析
  12. csv 20位数据 如何打开可以预览完整数字_条码打印软件如何批量制作MSI Plessey码...
  13. ASP.NET MVC 5 默认模板的JS和CSS 是怎么加载的?
  14. React项目以及降级兼容IE低版本
  15. word 2013 长篇文档排版案例教程
  16. 浅谈数据挖掘中的关联规则挖掘
  17. declares(declares是什么意思)
  18. WPF桌面应用实例(二):写一个扫雷游戏
  19. python3 陌生的角落(1):基础语法
  20. 【C++】2048游戏系列---优化模块第一稿【加载图片】

热门文章

  1. 双门限法语音端点检测(Python实现)
  2. qsnctf nice cream wp
  3. 机器视觉毕业设计 Python图像拼接算法研究与实现 - opencv
  4. 微信小程序开发之全屏显示
  5. Jmeter和jdk的下载和安装
  6. 线性代数知识总结梳理
  7. 游戏充值订单金额修改思路与实践
  8. 2017中国(上海)国际物联网大会在沪举办
  9. python毕业设计作品基于django框架 教室实验室预约系统毕设成品(8)毕业设计论文模板
  10. 等价类划分法与边界值分析法