Bugly iOS SDK 使用指南

SDK 集成

Bugly提供两种集成方式供iOS开发者选择:

  • 通过CocoaPods集成
  • 手动集成

如果您是从Bugly 2.0以下版本升级过来的,请查看iOS SDK 升级指南

Bugly iOS SDK 最低兼容系统版本 iOS 7.0

通过CocoaPods集成

在工程的Podfile里面添加以下代码:

1
pod 'Bugly'

保存并执行pod install,然后用后缀为.xcworkspace的文件打开工程。

注意:

命令行下执行pod search Bugly,如显示的Bugly版本不是最新的,则先执行pod repo update操作更新本地repo的内容

关于CocoaPods的更多信息请查看 CocoaPods官方网站

手动集成

  • 下载 Bugly iOS SDK
  • 拖拽Bugly.framework文件到Xcode工程内(请勾选Copy items if needed选项)
  • 添加依赖库
    • SystemConfiguration.framework
    • Security.framework
    • libz.dyliblibz.tbd
    • libc++.dyliblibc++.tbd

初始化SDK

导入头文件

在工程的AppDelegate.m文件导入头文件

1
#import <Bugly/Bugly.h>

如果是Swift工程,请在对应bridging-header.h中导入

初始化Bugly

在工程AppDelegate.mapplication:didFinishLaunchingWithOptions:方法中初始化:

  • Objective-C
1
2
3
4
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
[Bugly startWithAppId:@"此处替换为你的AppId"];
return YES;
}
  • Swift
1
2
3
4
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
Bugly.startWithAppId("此处替换为你的AppId")
return true
}

Bugly iOS 符号表配置(可选)

介绍

什么是符号表?

符号表是内存地址与函数名、文件名、行号的映射表。符号表元素如下所示:

1
<起始地址> <结束地址> <函数> [<文件名:行号>]

为什么要配置符号表?

为了能快速并准确地定位用户APP发生Crash的代码位置,Bugly使用符号表对APP发生Crash的程序堆栈进行解析还原

举一个例子:

Alt text

Bugly提供了自动和手动两种方法配置iOS符号表。

自动配置:XCode + sh脚本

自动配置请首先下载和解压自动配置符号表工具包,然后选择上传方式并配置Xcode的编译执行脚本。

上传方式

使用脚本自动配置支持两种上传方式:

  • 方式一:直接上传dSYM文件(默认方式 )
  • 方式二:提取dSYM文件的符号表文件并上传

其中,使用方式二需要额外操作以下几步:

  • 下载符号表提取工具依赖的Java运行环境(JRE或JDK版本需要>=1.6)
  • 把工具包buglySymbolIOS.jar保存在用户主目录(Home)的bin目录下(没有bin文件夹,请自行创建):

Alt text

配置Xcode编译执行脚本

  • 在Xcode工程对应Target的Build Phases中新增Run Scrpit Phase

Alt text

Alt text

  • 打开工具包中的dSYM_upload.sh,复制所有内容,在新增的Run Scrpit Phase中粘贴
  • 修改新增的Run Scrpit中的 <YOUR_APP_ID> 为您的App ID,<YOUR_APP_KEY>为您的App Key,<YOUR_BUNDLE_ID> 为App的Bundle Id

Alt text

脚本默认在Debug模式及模拟器编译情况下不会上传符号表,在需要上传的时候,请修改下列选项

  • Debug模式编译是否上传,1=上传 0=不上传,默认不上传

    UPLOAD_DEBUG_SYMBOLS=0

  • 模拟器编译是否上传,1=上传 0=不上传,默认不上传

    UPLOAD_SIMULATOR_SYMBOLS=0

至此,自动上传符号表脚本配置完毕,Bugly 会在每次 Xcode 工程编译后自动完成符号表配置工作。

手动配置

手动配置的流程如下:

  • 下载最新版Bugly iOS符号表工具,其中工具包中包括:
    • 符号表工具JAR包(buglySymboliOS.jar)
    • Windows的脚本(buglySymboliOS.bat)
    • Shell脚本(buglySymboliOS.sh)
    • 默认符号表配置文件(settings.txt)
    • 符号表工具iOS版-使用指南
  • 根据Crash的UUID定位dSYM文件
  • 使用工具生成符号表文件(zip文件)
  • 在页面上传符号表文件

Alt text

其他说明

  • Bugly iOS符号表工具2.3.0及以上版本增加了上传功能,2.5.0及以上版本支持dSYM文件的上传;
  • 定位dSYM文件的方法和工具的使用方法请参考:“符号表工具iOS版-使用指南”。

符号表上传接口

Bugly提供了上传符号表的API接口(使用POST方式上传):

HTTPS接口支持上传dSYM文件(需要压缩成Zip文件)和符号表文件(Symbol)。

参数说明

上传接口的参数说明如下:

属性 说明
api_version API版本,固定为1
app_id App ID
app_key App Key
symbolType 符号表类型,iOS为2
bundleId 包名(Package)
productVersion 版本号(Version Name)
fileName 符号表文件名
file 符号表文件

其中包名、版本号和符号表文件名需要做URL Encode。

例子:使用Curl上传

使用Curl工具来上传的例子如下:

  • 上传dSYM文件
1
curl -k "https://api.bugly.qq.com/openapi/file/upload/symbol?app_key=xxxxxx&app_id=xxxxxx" --form "api_version=1" --form "app_id=xxxxxx" --form "app_key=xxxxxx" --form "symbolType=2"  --form "bundleId=com.demo.test" --form "productVersion=1.0" --form "channel=xxx" --form "fileName=app.dSYM.zip" --form "file=@app.dSYM.zip" --verbose
  • 上传符号表文件(Symbol文件)
1
curl -k "https://api.bugly.qq.com/openapi/file/upload/symbol?app_key=xxxxxx&app_id=xxxxxx" --form "api_version=1" --form "app_id=xxxxxx" --form "app_key=xxxxxx" --form "symbolType=2"  --form "bundleId=com.demo.test" --form "productVersion=1.0" --form "fileName=symbol.zip" --form "file=@symbol.zip" --verbose

dSYM文件

什么是dSYM文件?

iOS平台中,dSYM文件是指具有调试信息的目标文件,文件名通常为:xxx.app.dSYM。如下图所示:

Alt text

1
为了方便找回Crash对应的dSYM文件和还原堆栈,建议每次构建或者发布APP版本的时候,备份好dSYM文件。

如何定位dSYM文件?

一般情况下,项目编译完dSYM文件跟app文件在同一个目录下,下面以XCode作为IDE详细说明定位dSYM文件。

1
2
3
4
5
-> 进入XCode;
-> 打开工程(已编译过);
-> 在左栏找到“Product”项;
-> 鼠标右键点击编译生成的“xxx.app”;
-> 点击“Show in Finder”;

如下图所示:

Alt text

Alt text

如果有多个dSYM文件,可以在使用工具时指定输入为dSYM文件所在的目录或者工程目录。

XCode编译后没有生成dSYM文件?

XCode Release编译默认会生成dSYM文件,而Debug编译默认不会生成,对应的Xcode配置如下:

1
2
XCode -> Build Settings -> Code Generation -> Generate Debug Symbols -> Yes
XCode -> Build Settings -> Build Option -> Debug Information Format -> DWARF with dSYM File

Alt text

开启Bitcode之后需要注意哪些问题?

  • 在点“Upload to App Store”上传到App Store服务器的时候需要声明符号文件(dSYM文件)的生成:

Alt text

  • 在配置符号表文件之前,需要从App Store中把该版本对应的dSYM文件下载回本地(参考“如何找回已发布到App Store的App对应的dSYM文件?”),然后用符号表工具生成和上传符号表文件。
  • 不需要配置自动生成符号表的脚本了,也不要用本地生成的dSYM文件来生成符号表文件,因为本地编译生成的dSYM文件的符号表信息都被隐藏了。如果用本地编译生成的dSYM文件生成符号表文件并配置到Bugly平台之后,还原出来的结果将是类似于“__hiden#XXX”这样的符号。

如何判断dSYM文件是否与Crash的UUID匹配?

Bugly还原Crash堆栈时,需要根据UUID来匹配符号表文件,因此只有上传的符号表文件的UUID与Crash对应APP的UUID一致时,才能准确地对堆栈进行还原。

Bugly v1.0页面

1
崩溃 ---> Crash issue ---> dSYM UUID

Alt text

Bugly v2.0页面

1
崩溃分析 ---> Crash issue ---> 符号表 ---> UUID

Alt text

如何查看dSYM文件的UUID?

通过命令查看UUID

1
xcrun dwarfdump --uuid <dSYM文件>

通过符号表文件查看UUID

符号表文件的UUID与dSYM文件的UUID是一致的,因此可以通过符号表工具生成的符号表文件来查看dSYM文件的UUID:

1
生成符号表文件(.zip) ---> 解压符号表文件(.symbol) ---> 使用文本编辑器打开符号表文件

Alt text

其中符号表文件的“UUID”信息即Debug SO文件的UUID,亦是符号表文件的UUID,如果文件较大,建议使用“Sublime Text”等文本编辑器来打开符号表文件。

如何找回已发布到App Store的App对应的dSYM文件?

通过Xcode找回

  1. 打开 Xcode 顶部菜单栏 -> Window -> Organizer 窗口: Alt text
  2. 打开 Xcode 顶部菜单栏,选择 Archive 标签: Alt text
  3. 找到发布的归档包,右键点击对应归档包,选择Show in Finder操作: Alt text
  4. 右键选择定位到的归档文件,选择显示包内容操作: Alt text
  5. 选择dSYMs目录,目录内即为下载到的 dSYM 文件: Alt text

通过iTunes Connect找回

  1. 登录iTunes Connect
  2. 进入“我的App(My Apps)”的“活动(Activity)”页面: Alt text
  3. 在“所有构件版本(All Builds)”中选择某一个版本,点“下载dSYM(Download dSYM)”下载dSYM文件: Alt text

通过mdfind工具找回

在Bugly的issue页面查询到crash对应的UUID:

然后在Mac的Shell中,用mdfind命令定位dSYM文件:

1
mdfind "com_apple_xcode_dsym_uuids == <UUID>"

注意,使用mdfind时,UUID需要格式转换(增加“-”): 12345678-1234-1234-1234-xxxxxxxxxxxx

例如,要定位的dSYM的UUID为:E30FC309DF7B3C9F8AC57F0F6047D65F 则定位dSYM文件的命令如下:

1
2
mdfind "com_apple_xcode_dsym_uuids == E30FC309-DF7B-3C9F-8AC5-7F0F6047D65F"
|12345678-1234-1234-1234-xxxxxxxxxxxx|

建议每次构建或者发布APP版本的时候,备份App对应的dSYM文件!

开启debug模式符号表上传功能(主要用于测试集成问题,可不需要)

XCode编译后生成dSYM文件设置

XCode Release编译默认会生成dSYM文件,而Debug编译默认不会生成,对应的Xcode配置如下:

  • XCode -> Build Settings -> Code Generation -> Generate Debug Symbols -> Yes

  • XCode -> Build Settings -> Build Option -> Debug Information Format -> DWARF with dSYM File

  • 脚本中 UPLOAD_DEBUG_SYMBOLS的值改为1

不需要debug模式上传的话,只需要还原到之前的设置就行了

官方文档地址 :Bugly iOS SDK 使用指南 Bugly iOS 符号表配置

通过bug报错的堆栈信息 和 符号表结合解析后能够快速的定位到类、方法甚至行号

bugly

封装颜色管理类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83

//
// FyColor.swift
// FyColor
//
// Created by l on 2020/3/13.
// Copyright © 2020 ifeiyv. All rights reserved.
//
import UIKit

class FyColors{

///深色模式适配 手动控制适配模式 启用 关闭(如非必要,可移除相关代码)
static let isOpenDarkModel:Bool = true


//MARK: eg文字颜色
//文字颜色 如果有多种文字颜色可以设置多个 eg: labelTextColor
public class var labelTextColor: UIColor {
return darkModeColor(dark:UIColor.white,light:UIColor.black)
}
//文字颜色 如果有多种文字颜色可以设置多个 eg: buttonTextColor
public class var buttonTextColor: UIColor {
return darkModeColor(dark:UIColor.white,light:UIColor.black)
}

//文字颜色 如果有多种文字颜色可以设置多个 eg: fieldTextColor
public class var fieldTextColor: UIColor {
return darkModeColor(dark:UIColor.white,light:UIColor.black)
}

//MARK: eg背景颜色
//背景颜色 如果有多种文字颜色可以设置多个 eg: labelBgColor
public class var labelBgColor: UIColor {
return darkModeColor(dark:UIColor.black,light:UIColor.white)
}
//背景颜色 如果有多种文字颜色可以设置多个 eg: buttonBgColor
public class var buttonBgColor: UIColor {
return darkModeColor(dark:UIColor.black,light:UIColor.white)
}

//背景颜色 如果有多种背景颜色可以设置多个 eg: viewBgColor
public class var viewBgColor: UIColor {
return darkModeColor(dark:UIColor.black,light:UIColor.white)
}

//.........................................
//根据需求增加相对应的颜色即可
//darkModeColor(dark:UIColor.white,light:UIColor.black)
//实际开发中不可能只有 UIColor.white,UIColor.black 两种颜色。
//根据产品需求增加和修改对应的颜色


//检测当前是否是深色模式
class func isDarkStyle() -> Bool{
if(!isOpenDarkModel){
return false
}
if #available(iOS 13.0, *){
let currentMode = UITraitCollection.current.userInterfaceStyle
if(currentMode == .dark){
return true
}
}
return false
}
// 适配 动态颜色
class func darkModeColor(dark:UIColor,light:UIColor) ->UIColor{
if(!isOpenDarkModel){
return light
}
if #available(iOS 13.0, *){
return UIColor{(trainCollection) -> UIColor in
if trainCollection.userInterfaceStyle == .dark{
return dark
}else{
return light
}
}
}
return light
}
}
使用方式:(深色模式切换时,系统会重新渲染颜色,自动在设置好的两种颜色中进行切换)
1
2
3
4
5
6
7
8
label.textColor = FyColors.labelTextColor

label.backgroundColor = FyColors.labelBgColor

//或者在此方法监听深色模式进行手动切换
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
super.traitCollectionDidChange(previousTraitCollection)
}
  1. ./build-ffmpeg.sh
1
2
3
4
5
6
7
8
xcrun -sdk iphoneos clang is unable to create an executable file.
C compiler test failed.

If you think configure made a mistake, make sure you are using the latest
version from Git. If the latest version fails, report the problem to the
ffmpeg-user@ffmpeg.org mailing list or IRC #ffmpeg on irc.freenode.net.
Include the log file "config.log" produced by configure as this will help
solve the problem.

解决方法:

1
sudo xcode-select --switch /Applications/Xcode.app
  1. armv7

https://www.jianshu.com/p/2669370bee23

1
2
3
./libavutil/arm/asm.S:50:9: error: unknown directive
.arch armv7-a
^

删除armv7compile-ffmpeg.sh文件中

1
2
3
FF_ALL_ARCHS_IOS8_SDK="armv7 arm64 i386 x86_64"

改为 FF_ALL_ARCHS_IOS8_SDK="arm64 i386 x86_64"

https://www.jianshu.com/p/65fb80dff4d6

合并真机和模拟器的framework

1
lipo -create 真机framework路径 模拟器framework路径 -output 合并的文件路径
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
import UIKit

class playerViewController: UIViewController {

var iPlayer:IJKFFMoviePlayerController?

override func viewDidLoad() {
super.viewDidLoad()

let options:IJKFFOptions = IJKFFOptions.byDefault()
let url:URL = URL.init(string: "rtmp://live.hkstv.hk.lxdns.com/live/hks")!


self.iPlayer = IJKFFMoviePlayerController.init(contentURL: url, with: options)
var arm1 = UIViewAutoresizing.init(rawValue: 0)
arm1.insert(UIViewAutoresizing.flexibleWidth)
arm1.insert(UIViewAutoresizing.flexibleHeight)
self.iPlayer?.view.autoresizingMask = arm1
self.iPlayer?.view.backgroundColor = UIColor.white
self.iPlayer?.view.frame = CGRect.init(x: 0, y: 0, width: UIScreen.main.bounds.size.width, height: 300)
self.iPlayer?.scalingMode = .aspectFit
self.iPlayer?.shouldAutoplay = true
self.view.autoresizesSubviews = true
self.view.addSubview((self.iPlayer?.view)!)

// Do any additional setup after loading the view, typically from a nib.
}


override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}


override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
self.iPlayer?.prepareToPlay() //准备
self.iPlayer?.play() //播放
}


override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
self.iPlayer?.pause()//暂停
// self.iPlayer?.shutdown() //销毁
}

}

LFLiveKit:框架支持RTMP,由Adobe公司开发。github地址https://github.com/LaiFengiOS/LFLiveKit

LFLiveKit库里已经集成GPUImage框架用于美颜功能,GPUImage基于OpenGl开发,纯OC语言框架,封装好了各种滤镜同时也可以编写自定义的滤镜,其本身内置了多达125种常见的滤镜效果。

LFLiveKit库通过pod导入项目

1
pod 'LFLiveKit'

配置上传地址

1
2
3
let stream = LFLiveStreamInfo()
stream.url = "rtmp://192.168.***.***:1935/rtmplive/room"
session.startLive(stream)

demo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
//
// ViewController.swift
// IfeiyvLiveVideo
//
// Created by l on 2019/7/1.
// Copyright © 2019 ifeiyv. All rights reserved.
//

import UIKit

class ViewController: UIViewController, LFLiveSessionDelegate {

override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.

session.delegate = self
session.preView = self.view

self.requestAccessForVideo()
self.requestAccessForAudio()
self.view.backgroundColor = UIColor.clear
self.view.addSubview(containerView)
containerView.addSubview(stateLabel)
containerView.addSubview(closeButton)
containerView.addSubview(beautyButton)
containerView.addSubview(cameraButton)
containerView.addSubview(startLiveButton)

cameraButton.addTarget(self, action: #selector(didTappedCameraButton(_:)), for:.touchUpInside)
beautyButton.addTarget(self, action: #selector(didTappedBeautyButton(_:)), for: .touchUpInside)
startLiveButton.addTarget(self, action: #selector(didTappedStartLiveButton(_:)), for: .touchUpInside)
}

override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}

//MARK: AccessAuth

func requestAccessForVideo() -> Void {
let status = AVCaptureDevice.authorizationStatus(for: AVMediaType.video);
switch status {
// 许可对话没有出现,发起授权许可
case AVAuthorizationStatus.notDetermined:
AVCaptureDevice.requestAccess(for: AVMediaType.video, completionHandler: { (granted) in
if(granted){
DispatchQueue.main.async {
self.session.running = true
}
}
})
break;
// 已经开启授权,可继续
case AVAuthorizationStatus.authorized:
session.running = true;
break;
// 用户明确地拒绝授权,或者相机设备无法访问
case AVAuthorizationStatus.denied: break
case AVAuthorizationStatus.restricted:break;
default:
break;
}
}

func requestAccessForAudio() -> Void {
let status = AVCaptureDevice.authorizationStatus(for:AVMediaType.audio)
switch status {
// 许可对话没有出现,发起授权许可
case AVAuthorizationStatus.notDetermined:
AVCaptureDevice.requestAccess(for: AVMediaType.audio, completionHandler: { (granted) in

})
break;
// 已经开启授权,可继续
case AVAuthorizationStatus.authorized:
break;
// 用户明确地拒绝授权,或者相机设备无法访问
case AVAuthorizationStatus.denied: break
case AVAuthorizationStatus.restricted:break;
default:
break;
}
}

//MARK: - Callbacks

// 回调
func liveSession(_ session: LFLiveSession?, debugInfo: LFLiveDebug?) {
print("debugInfo: \(debugInfo?.currentBandwidth)")
}

func liveSession(_ session: LFLiveSession?, errorCode: LFLiveSocketErrorCode) {
print("errorCode: \(errorCode.rawValue)")
}

func liveSession(_ session: LFLiveSession?, liveStateDidChange state: LFLiveState) {
print("liveStateDidChange: \(state.rawValue)")
switch state {
case LFLiveState.ready:
stateLabel.text = "未连接"
break;
case LFLiveState.pending:
stateLabel.text = "连接中"
break;
case LFLiveState.start:
stateLabel.text = "已连接"
break;
case LFLiveState.error:
stateLabel.text = "连接错误"
break;
case LFLiveState.stop:
stateLabel.text = "未连接"
break;
default:
break;
}
}

//MARK: - Events

// 开始直播
@objc func didTappedStartLiveButton(_ button: UIButton) -> Void {
startLiveButton.isSelected = !startLiveButton.isSelected;
if (startLiveButton.isSelected) {
startLiveButton.setTitle("结束直播", for: UIControl.State())
let stream = LFLiveStreamInfo()
stream.url = "rtmp://192.168.1.148:1935/rtmplive/room"
session.startLive(stream)
} else {
startLiveButton.setTitle("开始直播", for: UIControl.State())
session.stopLive()
}
}

// 美颜
@objc func didTappedBeautyButton(_ button: UIButton) -> Void {
session.beautyFace = !session.beautyFace;
beautyButton.isSelected = !session.beautyFace
}

// 摄像头
@objc func didTappedCameraButton(_ button: UIButton) -> Void {
let devicePositon = session.captureDevicePosition;
session.captureDevicePosition = (devicePositon == AVCaptureDevice.Position.back) ? AVCaptureDevice.Position.front : AVCaptureDevice.Position.back;
}

// 关闭
func didTappedCloseButton(_ button: UIButton) -> Void {

}

//MARK: - Getters and Setters

//  默认分辨率368 * 640 音频:44.1 iphone6以上48 双声道 方向竖屏
var session: LFLiveSession = {
let audioConfiguration = LFLiveAudioConfiguration.defaultConfiguration(for: LFLiveAudioQuality.high)
let videoConfiguration = LFLiveVideoConfiguration.defaultConfiguration(for: LFLiveVideoQuality.low3)
let session = LFLiveSession(audioConfiguration: audioConfiguration, videoConfiguration: videoConfiguration)
return session!
}()

// 视图
var containerView: UIView = {
let containerView = UIView(frame: CGRect(x: 0, y: 0, width: UIScreen.main.bounds.width, height: UIScreen.main.bounds.height))
containerView.backgroundColor = UIColor.clear
containerView.autoresizingMask = [UIView.AutoresizingMask.flexibleHeight, UIView.AutoresizingMask.flexibleHeight]
return containerView
}()

// 状态Label
var stateLabel: UILabel = {
let stateLabel = UILabel(frame: CGRect(x: 20, y: 20, width: 80, height: 40))
stateLabel.text = "未连接"
stateLabel.textColor = UIColor.white
stateLabel.font = UIFont.systemFont(ofSize: 14)
return stateLabel
}()

// 关闭按钮
var closeButton: UIButton = {
let closeButton = UIButton(frame: CGRect(x: UIScreen.main.bounds.width - 10 - 44, y: 20, width: 44, height: 44))
closeButton.setImage(UIImage(named: "close_preview"), for: UIControl.State())
return closeButton
}()

// 摄像头
var cameraButton: UIButton = {
let cameraButton = UIButton(frame: CGRect(x: UIScreen.main.bounds.width - 54 * 2, y: 20, width: 44, height: 44))
cameraButton.setImage(UIImage(named: "camra_preview"), for: UIControl.State())
return cameraButton
}()

// 摄像头
var beautyButton: UIButton = {
let beautyButton = UIButton(frame: CGRect(x: UIScreen.main.bounds.width - 54 * 3, y: 20, width: 44, height: 44))
beautyButton.setImage(UIImage(named: "camra_beauty"), for: UIControl.State.selected)
beautyButton.setImage(UIImage(named: "camra_beauty_close"), for: UIControl.State())
return beautyButton
}()

// 开始直播按钮
var startLiveButton: UIButton = {
let startLiveButton = UIButton(frame: CGRect(x: 30, y: UIScreen.main.bounds.height - 50, width: UIScreen.main.bounds.width - 10 - 44, height: 44))
startLiveButton.layer.cornerRadius = 22
startLiveButton.setTitleColor(UIColor.black, for:UIControl.State())
startLiveButton.setTitle("开始直播", for: UIControl.State())
startLiveButton.titleLabel!.font = UIFont.systemFont(ofSize: 14)
startLiveButton.backgroundColor = UIColor(red: 50/255.0, green: 32/255.0, blue: 245/255.0, alpha: 1)//colorLiteralRed: 50, green: 32, blue: 245, alpha: 1
return startLiveButton
}()
}
//
// ViewController.swift
// IfeiyvLiveVideo
//
// Created by l on 2019/7/1.
// Copyright © 2019 ifeiyv. All rights reserved.
//

import UIKit

class ViewController: UIViewController, LFLiveSessionDelegate {

override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.

session.delegate = self
session.preView = self.view

self.requestAccessForVideo()
self.requestAccessForAudio()
self.view.backgroundColor = UIColor.clear
self.view.addSubview(containerView)
containerView.addSubview(stateLabel)
containerView.addSubview(closeButton)
containerView.addSubview(beautyButton)
containerView.addSubview(cameraButton)
containerView.addSubview(startLiveButton)

cameraButton.addTarget(self, action: #selector(didTappedCameraButton(_:)), for:.touchUpInside)
beautyButton.addTarget(self, action: #selector(didTappedBeautyButton(_:)), for: .touchUpInside)
startLiveButton.addTarget(self, action: #selector(didTappedStartLiveButton(_:)), for: .touchUpInside)
}

override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}

//MARK: AccessAuth

func requestAccessForVideo() -> Void {
let status = AVCaptureDevice.authorizationStatus(for: AVMediaType.video);
switch status {
// 许可对话没有出现,发起授权许可
case AVAuthorizationStatus.notDetermined:
AVCaptureDevice.requestAccess(for: AVMediaType.video, completionHandler: { (granted) in
if(granted){
DispatchQueue.main.async {
self.session.running = true
}
}
})
break;
// 已经开启授权,可继续
case AVAuthorizationStatus.authorized:
session.running = true;
break;
// 用户明确地拒绝授权,或者相机设备无法访问
case AVAuthorizationStatus.denied: break
case AVAuthorizationStatus.restricted:break;
default:
break;
}
}

func requestAccessForAudio() -> Void {
let status = AVCaptureDevice.authorizationStatus(for:AVMediaType.audio)
switch status {
// 许可对话没有出现,发起授权许可
case AVAuthorizationStatus.notDetermined:
AVCaptureDevice.requestAccess(for: AVMediaType.audio, completionHandler: { (granted) in

})
break;
// 已经开启授权,可继续
case AVAuthorizationStatus.authorized:
break;
// 用户明确地拒绝授权,或者相机设备无法访问
case AVAuthorizationStatus.denied: break
case AVAuthorizationStatus.restricted:break;
default:
break;
}
}

//MARK: - Callbacks

// 回调
func liveSession(_ session: LFLiveSession?, debugInfo: LFLiveDebug?) {
print("debugInfo: \(debugInfo?.currentBandwidth)")
}

func liveSession(_ session: LFLiveSession?, errorCode: LFLiveSocketErrorCode) {
print("errorCode: \(errorCode.rawValue)")
}

func liveSession(_ session: LFLiveSession?, liveStateDidChange state: LFLiveState) {
print("liveStateDidChange: \(state.rawValue)")
switch state {
case LFLiveState.ready:
stateLabel.text = "未连接"
break;
case LFLiveState.pending:
stateLabel.text = "连接中"
break;
case LFLiveState.start:
stateLabel.text = "已连接"
break;
case LFLiveState.error:
stateLabel.text = "连接错误"
break;
case LFLiveState.stop:
stateLabel.text = "未连接"
break;
default:
break;
}
}

//MARK: - Events

// 开始直播
@objc func didTappedStartLiveButton(_ button: UIButton) -> Void {
startLiveButton.isSelected = !startLiveButton.isSelected;
if (startLiveButton.isSelected) {
startLiveButton.setTitle("结束直播", for: UIControl.State())
let stream = LFLiveStreamInfo()
stream.url = "rtmp://192.168.1.148:1935/rtmplive/room"
session.startLive(stream)
} else {
startLiveButton.setTitle("开始直播", for: UIControl.State())
session.stopLive()
}
}

// 美颜
@objc func didTappedBeautyButton(_ button: UIButton) -> Void {
session.beautyFace = !session.beautyFace;
beautyButton.isSelected = !session.beautyFace
}

// 摄像头
@objc func didTappedCameraButton(_ button: UIButton) -> Void {
let devicePositon = session.captureDevicePosition;
session.captureDevicePosition = (devicePositon == AVCaptureDevice.Position.back) ? AVCaptureDevice.Position.front : AVCaptureDevice.Position.back;
}

// 关闭
func didTappedCloseButton(_ button: UIButton) -> Void {

}

//MARK: - Getters and Setters

//  默认分辨率368 * 640 音频:44.1 iphone6以上48 双声道 方向竖屏
var session: LFLiveSession = {
let audioConfiguration = LFLiveAudioConfiguration.defaultConfiguration(for: LFLiveAudioQuality.high)
let videoConfiguration = LFLiveVideoConfiguration.defaultConfiguration(for: LFLiveVideoQuality.low3)
let session = LFLiveSession(audioConfiguration: audioConfiguration, videoConfiguration: videoConfiguration)
return session!
}()

// 视图
var containerView: UIView = {
let containerView = UIView(frame: CGRect(x: 0, y: 0, width: UIScreen.main.bounds.width, height: UIScreen.main.bounds.height))
containerView.backgroundColor = UIColor.clear
containerView.autoresizingMask = [UIView.AutoresizingMask.flexibleHeight, UIView.AutoresizingMask.flexibleHeight]
return containerView
}()

// 状态Label
var stateLabel: UILabel = {
let stateLabel = UILabel(frame: CGRect(x: 20, y: 20, width: 80, height: 40))
stateLabel.text = "未连接"
stateLabel.textColor = UIColor.white
stateLabel.font = UIFont.systemFont(ofSize: 14)
return stateLabel
}()

// 关闭按钮
var closeButton: UIButton = {
let closeButton = UIButton(frame: CGRect(x: UIScreen.main.bounds.width - 10 - 44, y: 20, width: 44, height: 44))
closeButton.setImage(UIImage(named: "close_preview"), for: UIControl.State())
return closeButton
}()

// 摄像头
var cameraButton: UIButton = {
let cameraButton = UIButton(frame: CGRect(x: UIScreen.main.bounds.width - 54 * 2, y: 20, width: 44, height: 44))
cameraButton.setImage(UIImage(named: "camra_preview"), for: UIControl.State())
return cameraButton
}()

// 摄像头
var beautyButton: UIButton = {
let beautyButton = UIButton(frame: CGRect(x: UIScreen.main.bounds.width - 54 * 3, y: 20, width: 44, height: 44))
beautyButton.setImage(UIImage(named: "camra_beauty"), for: UIControl.State.selected)
beautyButton.setImage(UIImage(named: "camra_beauty_close"), for: UIControl.State())
return beautyButton
}()

// 开始直播按钮
var startLiveButton: UIButton = {
let startLiveButton = UIButton(frame: CGRect(x: 30, y: UIScreen.main.bounds.height - 50, width: UIScreen.main.bounds.width - 10 - 44, height: 44))
startLiveButton.layer.cornerRadius = 22
startLiveButton.setTitleColor(UIColor.black, for:UIControl.State())
startLiveButton.setTitle("开始直播", for: UIControl.State())
startLiveButton.titleLabel!.font = UIFont.systemFont(ofSize: 14)
startLiveButton.backgroundColor = UIColor(red: 50/255.0, green: 32/255.0, blue: 245/255.0, alpha: 1)//colorLiteralRed: 50, green: 32, blue: 245, alpha: 1
return startLiveButton
}()
}

在网上搜索参考了大量文章,解决了N多Bug,终于实现了直播功能

nginx是非常优秀的开源服务器,用它来做hls或者rtmp流媒体服务器是非常不错的选择

1、安装Homebrow

(1)执行克隆命令,github的项目(https://github.com/denji/homebrew-nginx)

1
brew tap denji/nginx

注意brew tap homebrew/nginx报下面的错误,homebrew/nginx已经弃用. 报错:Error: homebrew/nginx was deprecated. This tap is now empty as all its formulae were migrated.

(2)执行安装命令:

1
brew install nginx-full --with-rtmp-module

(3)至此nginx和rtmp模块就安装好了,下面开始来配置nginx的rtmp模块
接下来看一下nginx安装在什么地方:

1
brew info nginx-full

打印信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
The default port has been set in /usr/local/etc/nginx/nginx.conf to 8080 so that
nginx can run without sudo.

nginx will load all files in /usr/local/etc/nginx/servers/.

- Tips -
Run port 80:
$ sudo chown root:wheel /usr/local/opt/nginx-full/bin/nginx
$ sudo chmod u+s /usr/local/opt/nginx-full/bin/nginx
Reload config:
$ nginx -s reload
Reopen Logfile:
$ nginx -s reopen
Stop process:
$ nginx -s stop
Waiting on exit process
$ nginx -s quit

To have launchd start denji/nginx/nginx-full now and restart at login:
brew services start denji/nginx/nginx-full
Or, if you don't want/need a background service you can just run:
nginx

nginx安装所在位置:

1
/usr/local/opt/nginx-full/bin/nginx

nginx配置文件所在位置:

1
/usr/local/etc/nginx/nginx.conf

(4)启动nginx,执行命令:

1
sudo  nginx

2、测试nginx:

1
在浏览器中打开如下地址:http://localhost:8080入过

如果出现Welcome to nginx!,说明安装成功.

如果终端上提示:

1
nginx: [emerg] bind() to 0.0.0.0:8080 failed (48: Address already in use)

则表示8080端口被占用了, 查看端口PID

1
lsof -i tcp:8080

kill掉占用8080端口的PID

1
kill 9603(这里替换成占用8080端口的PID)

3、重新加载nginx的配置文件

(1)修改nginx.conf这个配置文件,配置rtmp
复制nginx配置文件所在位置:

1
vi /usr/local/etc/nginx/nginx.conf

(2)执行上面命令会直接编辑,或者直接前往当前文件用记事本打开.
滚动到最后面(最后一个}后面即可,不能在{}里面),添加一下代码,进行配置,最后记得保存。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
rtmp {  
server {
listen 1935;
#直播流配置
application rtmplive {
live on;
#为 rtmp 引擎设置最大连接数。默认为 off
max_connections 1024;
}
application hls{
live on;
hls on;
hls_path /usr/local/var/www/hls;
hls_fragment 1s;
}
}
}

(3)编辑完成之后,执行一下重新加载配置文件命令:

1
sudo nginx -s reload

需要输入开机密码 sudo不加的话会报错: nginx: [alert] could not open error log file: open() "/usr/local/var/log/nginx/error.log" failed (13: Permission denied)该命令执行后会出来一个弹框询问是否允许 nginx 加入到网络中,选择允许即可。

(4)重启nginx:

1
sudo /usr/local/opt/nginx-full/bin/nginx -s reload

PS:如果你之前不是按照我上面的方法按照的 nginx,在执行 sudo nginx -s reload 时报了如下错,建议你卸载 nginx后按照我上面的步骤重新安装nginx。
nginx: [emerg] unknown directive “rtmp” in /usr/local/etc/nginx/nginx.conf:119

nginx常用方法:

出现权限不足的错误提示时,命令前加上 sudo

1
2
3
4
重新加载配置文件:  nginx -s reload
重新加载日志: nginx -s reopen
停止 nginx: nginx -s stop
有序退出 nginx: nginx -s quit

4、安装ffmepg工具

1
brew install ffmpeg

5、本地推流

(1)、搭建本地视频直播,比如电脑上面有很多电影,我们可以通过推流的形式实现实时直播:

A:在电脑上播放推流内容
安装一个支持rtmp协议的视频播放器,Mac下可以用VLC
下载VLC

本地下载一个视频文件路径为 /Users/iOS002/Desktop/loginmovie.mp4

执行以下命令:

1
ffmpeg -re -i /Users/iOS002/Desktop/loginmovie.mp4  -vcodec libx264 -acodec aac -strict -2 -f flv rtmp://localhost:1935/rtmplive/room

用vlc 然后打开 VLC 中 的 file – Open Network, 直接输入代码中的 url:

1
rtmp://localhost:1935/rtmplive/room

即可以通过VLC来播放终端中实时推过来的 RTMP流。

B:通过手机观看电脑的推流

通过集成 ijkplayer 把地址换成推流的地址即可观看

播放端用的针对RTMP优化过的ijkplayer,ijkplayer是基于FFmpeg的跨平台播放器,这个开源项目已经被多个 App 使用,其中映客、美拍和斗鱼使用了 ijkplayer。

(2)、桌面录制或者分享

1
ffmpeg -f avfoundation -i "1" -vcodec libx264 -preset ultrafast -acodec libfaac -f flv rtmp://localhost:1935/rtmplive/room

(3)、桌面+麦克风

1
ffmpeg -f avfoundation -i "1:0" -vcodec libx264 -preset ultrafast -acodec libmp3lame -ar 44100 -ac 1 -f flv rtmp://localhost:1935/rtmplive/room

(4)、桌面+麦克风,并且还要摄像头拍摄到自己

1
ffmpeg -f avfoundation -framerate 30 -i "1:0" \-f avfoundation -framerate 30 -video_size 640x480 -i "0" \-c:v libx264 -preset ultrafast \-filter_complex 'overlay=main_w-overlay_w-10:main_h-overlay_h-10' -acodec libmp3lame -ar 44100 -ac 1 -f flv rtmp://localhost:2016/rtmplive/room

6、手机推流

可以用 LFLiveKit 集成到工程进行推流,LFLiveKit已经帮我们实现了视频采集、后台录制、美颜功能、支持h264、AAC编码,动态改变速率,RTMP传输等,我们开发的时候就很简单了只需把localhost:8080换成自己电脑的ip地址即可:

1
rtmp://10.0.0.17:1935/rtmplive/room

注意通过网络查看电脑的局域网 IP替换掉 localhost 即可。

A:通过VLC观看手机的推流

打开手机直播后,然后在电脑上打开VLC(同上),就能实现手机推流,在电脑上拉流播放了!!(注:手机需要和电脑连接同一网络!)

B:通过手机观看手机的推流(这也就是市面上的那些直播App的最终实现形式了)

通过集成 ijkplayer 把地址换成推流的地址即可观看。

PS:一个很隐蔽的报错:

如果你发现你的推流地址和拉流地址在电脑上都是好好的,但是通过手机实现的时候就是报错,那么估计就是因为Mac防火墙的问题。

1
2
3
4
ERROR: PILI_RTMP_Connect0, failed to connect socket. 60 (Operation timed out)
ERROR: WriteN, PILI_RTMP send error 9, Bad file descriptor, (140 bytes)
ERROR: PILI_RTMP_Connect0, failed to connect socket. 60 (Operation timed out)
ERROR: WriteN, PILI_RTMP send error 9, Bad file descriptor, (140 bytes)

关闭 Mac 的防火墙即可解决问题。

1
偏好设置->安全性与隐私->防火墙