Tips:这次的内容分为两篇文章讲述
01、[iOS]仿微博视频边下边播之封装播放器 讲述如何封装一个实现了边下边播并且缓存的视频播放器。
02、[iOS]仿微博视频边下边播之滑动TableView自动播放 讲述如何实现在tableView中滑动播放视频,并且是流畅,不阻塞线程,没有任何卡顿的实现滑动播放视频。同时也将讲述当tableView滚动时,以什么样的策略,来确定究竟哪一个cell应该播放视频。
上篇文章讲述了封装一个边下边播,并且带有缓存功能的播放器。如果你还没有看,请点击跳转[iOS]仿微博视频边下边播之封装播放器 。接下来,讲述如何将这个播放器应用到tableView里。并且达到如下效果。
01、dispatch_semaphore信号量?
dispatch_semaphore 信号量基于计数器的一种多线程同步机制。在多个线程访问共有资源时候,会因为多线程的特性而引发数据出错的问题。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
dispatch_queue_t
queue
=
dispatch_get_global_queue
(
0
,
0
)
;
// “创建方法里会传入一个long型的参数,这个东西你可以想象是一个库存”
dispatch_semaphore_t
semaphore
=
dispatch_semaphore_create
(
1
)
;
NSMutableArray
*array
=
[
NSMutableArray
array
]
;
for
(
int
index
=
0
;
index
<
10000
;
index
++
)
{
dispatch_async
(
queue
,
^
(
)
{
// “每运行一次,会先清一个库存,如果库存为0,那么根据传入的等待时间,决定等待增加库存的时间
//如果设置为DISPATCH_TIME_FOREVER,那么意思就是永久等待增加库存,否则就永远不往下面走”
dispatch_semaphore_wait
(
semaphore
,
DISPATCH_TIME_FOREVER
)
;
NSLog
(
@"addd :%d"
,
index
)
;
[
array
addObject
:
[
NSNumber
numberWithInt
:index
]
]
;
// 每运行一次,增加一个库存
dispatch_semaphore_signal
(
semaphore
)
;
}
)
;
}
|
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
如果semaphore计数大于等于1.计数-1,返回,程序继续运行。
如果计数为0,则等待。
这里设置的等待时间是一直等待。
dispatch_semaphore_signal(semaphore);
计数+1.
在这两句代码中间的执行代码,每次只会允许一个线程进入,这样就有效的保证了在多线程环境下,只能有一个线程进入。
- AVPlayer底层是有信号量等待的特性的。具体表现在,“AVPlayer的replaceCurrentItemWithPlayerItem(用来切换视频的)方法在切换视频时底层会调用信号量等待,然后导致当前线程卡顿,如果在UITableViewCell中切换视频播放使用这个方法,会导致当前线程冻结几秒钟。” 这里说的线程是UI线程,即主线程,主线程冻结的结果就是主线程阻塞,带来卡顿和不流畅。
- 你可能会想,那就不要在主线程切换视频,不就不卡顿主线程了吗?如果你这么做,那你就不能保证视频播放是及时响应的。同时,因为子线程你不知道什么时候调用,你也不能保证你能及时关闭不需要播放的视频。也就是说,如果基于以上思路,当你滑动tableView时,可能会出现多个cell同时播放视频,而且会出现你要播的播不了,你想掐死的掐不死。
02、切换视频解决方案?
在tableView里播放视频,当用户滑动时,肯定是频繁切换视频的。上面讲了使用AVPlayer自带的replaceCurrentItemWithPlayerItem来切换视频带来的问题,我们得出的结论是:
- 不能使用replaceCurrentItemWithPlayerItem方法切换视频。
- 不能在子线程切换视频。
解决方案一
当出现这种问题的时候,我只能跑到官方文档去找答案了。
1
|
@
interface
AVQueuePlayer
:
AVPlayer
|
我在官方文档里找到AVQueuePlayer,他是AVPlayer的一个子类,他会自己维护一个播放队列。并且提供方法,可以插入播放条目,也可以移除播放条目,然后切换视频。
1
2
3
4
5
6
7
8
9
10
11
12
|
// 创建AVQueuePlayer
NSArray *
items
=
;
AVQueuePlayer *
queuePlayer
=
[
[
AVQueuePlayer
alloc
]
initWithItems
:
items
]
;
// 插入item
-
(
void
)
insertItem
:
(
AVPlayerItem *
)
item
afterItem
:
(
nullable
AVPlayerItem *
)
afterItem
;
// 移除item
-
(
void
)
removeItem
:
(
AVPlayerItem *
)
item
;
// 切换视频
-
(
void
)
advanceToNextItem
;
|
这个类还是很好使的,可以在不卡顿主线程的情况下流畅切换视频。但是他有他的坑,是我多次试验以后发现的,就是重播视频的时候,如果你放在那里不动,他大概会重复播放十次左右,然后播放器就莫名其妙的死掉了,这个时候你没有办法重新唤醒他。至于什么原因导致的,我暂时还没有找到。如果你知道,请你务必在下面留言,让更多人看到。
解决方案二
上面的那个方案被我否了,接下来,我采取的是尝试每次切换视频的时候都重新创建播放器,重新建立网络请求。总之,就是所有的配置都重新创建。刚开始的时候,我担心这样会造成处理器负担,但是实际使用起来,发现并没有任何性能问题。但是前提是,在重新创建之前,把所有的请求释放掉,同时把之前的播放器也释放,还有预览图层也释放。
03、重播?
先来看一下AVFoundation下用来表示时间的结构体CMTime,AVFoundation下的时间刻度是以最精准的分数形式来表示的。他有两个重要的值,value表示分子,timescale表示分母。
1
2
3
4
5
6
7
|
typedef
struct
{
CMTimeValue
value
;
// 分子
CMTimeScale
timescale
;
// 分母
CMTimeFlags
flags
;
CMTimeEpoch
epoch
;
}
CMTime
;
|
比如说我们要表示视频的起点,就是0秒,那就可以写成CMTimeMake(0, 1)。
我们的播放器是支持自动重播的,所以我们要在系统的通知中心监听播放器播放完成的通知,在接收到通知后,进行对应的处理。
1
2
3
4
5
6
7
8
9
10
11
12
|
// 监听播放结束通知
[
[
NSNotificationCenter
defaultCenter
]
addObserver
:
self
selector
:
@
selector
(
playerItemDidPlayToEnd
:
)
name
:
AVPlayerItemDidPlayToEndTimeNotification
object
:
nil
]
;
-
(
void
)
playerItemDidPlayToEnd
:
(
NSNotification *
)
notification
{
// 重复播放, 从起点开始重播, 没有内存暴涨
__weak
typeof
(
self
)
weak_self
=
self
;
[
self
.
player
seekToTime
:
CMTimeMake
(
0
,
1
)
completionHandler
:
^
(
BOOL
finished
)
{
__strong
typeof
(
weak_self
)
strong_self
=
weak_self
;
if
(
!
strong_self
)
return
;
[
strong_self
.
player
play
]
;
}
]
;
}
|
04、滑动tableView自动播放?
首先是一启动,应该自动去可见cell中查找第一个需要播放视频的cell,如果找到就开始播放。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
-
(
void
)
playVideoInVisiableCells
{
NSArray *
visiableCells
=
[
self
.
tableView
visibleCells
]
;
// 在可见cell中找到第一个有视频的cell
JPVideoPlayerCell *
videoCell
=
nil
;
for
(
JPVideoPlayerCell *
cell
in
visiableCells
)
{
if
(
cell
.
videoPath
.
length
>
0
)
{
videoCell
=
cell
;
break
;
}
}
// 如果找到了, 就开始播放视频
if
(
videoCell
)
{
self
.
playingCell
=
videoCell
;
self
.
currentVideoPath
=
videoCell
.
videoPath
;
JPVideoPlayer *
player
=
[
JPVideoPlayer
sharedInstance
]
;
[
player
playWithUrl
:
[
NSURL
URLWithString
:
videoCell
.
videoPath
]
showView
:
videoCell
.
containerView
]
;
player
.
mute
=
YES
;
}
}
|
接下来就是滚动tableView的时候,播放视频。在做之前,首先,我们要先制定一个规则来确定,滚动的时候究竟哪一个cell应该播放视频。我画了一张图,一起来看一下。
我的规则是:当tableView滑动的时候,我会播放可见cell中,最靠近屏幕中心的那个cell的视频。如果最靠近屏幕中心的那个cell没有视频,就会按照这个规则去其他可见cell中找,如果都没有找到,就不播放视频。规则有了,接下来就是去实现。其实,实现的时候,我们应该换一个思路,就是,只有那个cell需要播放视频,才会参与筛选是否是最靠近屏幕中心的cell。这样就避免了递归查找。
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
|
-
(
void
)
handleScroll
{
// 找到下一个要播放的cell(最在屏幕中心的)
JPVideoPlayerCell
*finnalCell
=
nil
;
NSArray
*visiableCells
=
[
self
.
tableView
visibleCells
]
;
NSMutableArray
*indexPaths
=
[
NSMutableArray
array
]
;
CGFloat
gap
=
MAXFLOAT
;
for
(
JPVideoPlayerCell
*cell
in
visiableCells
)
{
[
indexPaths
addObject
:cell
.
indexPath
]
;
if
(
cell
.
videoPath
.
length
>
0
)
{
// 如果这个cell有视频
CGPoint
coorCentre
=
[
cell
.
superview
convertPoint
:cell
.
center
toView
:nil
]
;
CGFloat
delta
=
fabs
(
coorCentre
.
y
-
[
UIScreen
mainScreen
]
.
bounds
.
size
.
height
*
0.5
)
;
if
(
delta
<
gap
)
{
gap
=
delta
;
finnalCell
=
cell
;
}
}
}
// 注意, 如果正在播放的cell和finnalCell是同一个cell, 不应该在播放
if
(
finnalCell
!=
nil
&&
self
.
playingCell
!=
finnalCell
)
{
[
[
JPVideoPlayer
sharedInstance
]
stop
]
;
[
[
JPVideoPlayer
sharedInstance
]
playWithUrl
:
[
NSURL
URLWithString
:finnalCell
.
videoPath
]
showView
:finnalCell
.
containerView
]
;
self
.
playingCell
=
finnalCell
;
self
.
currentVideoPath
=
finnalCell
.
videoPath
;
[
JPVideoPlayer
sharedInstance
]
.
mute
=
YES
;
return
;
}
// 再看正在播放视频的那个cell移出视野, 则停止播放
BOOL
isPlayingCellVisiable
=
YES
;
if
(
!
[
indexPaths
containsObject
:self
.
playingCell
.
indexPath
]
)
{
isPlayingCellVisiable
=
NO
;
}
if
(
!
isPlayingCellVisiable
&&
self
.
playingCell
)
{
[
self
stopPlay
]
;
}
}
|
这里没有难点,唯一可以讲一下的就是坐标之间的转换。我们拿到的cell的中心点的坐标是tableView的坐标,但是我们计算各个中心点离屏幕中心点之间的距离,用的是屏幕Window的坐标,所以要先将这个中心点的坐标转换为屏幕的坐标,再进行计算。但是,你也可以不转换坐标,因为他们是同一个坐标系的(tableView的坐标系)。可是,我个人的编程习惯是先转换,再计算。因为我觉得会比较清晰一点。尤其是当我们做复杂的过渡动画的时候,有这个意识,你会发现条理会很清晰。
05、什么时候播?
你肯定告诉我,滑动的时候播。这个回答是正确的,但是也是不正确的,因为我们尝试用编程的思想来思考这个问题。tableView的滚动过程分为两种情况:
- 将要开始拖动 –> 开始拖动 –> 滚动 –> 松手 –> 静止 –> 结束
- 将要开始拖动 –> 开始拖动 –> 滚动 –> 松手 –> 开始减速 –> 减速完成 –> 静止 –> 结束
首先要肯定的是,不能在滚动的时候调用视频播放的逻辑。这一点应该没有异议。原因是,第一,这个方法会来很多很多次,而且从实现上来说,不可能一调用滚动的代理就实现播放。第二从用户角度来说,在滑动的时候,他自己也没有决定要看哪一个cell。所以在滚动时,我们不做反应。
其次是开始拖动时,也不要作反应,因为,这个时候作反应没有意义。松手的时候,因为有松手时静止和松手时滚动两种情况,所以我不做处理。
逐渐排除下来,最后,适合调用播放逻辑的,只剩下“静止”这个动作了。我们来看一下静止对应的代理方法:
1
2
3
4
5
6
7
8
9
10
11
12
|
// 松手时已经静止,只会调用scrollViewDidEndDragging
-
(
void
)
scrollViewDidEndDragging
:
(
UIScrollView *
)
scrollView
willDecelerate
:
(
BOOL
)
decelerate
{
if
(
decelerate
==
NO
)
{
// scrollView已经完全静止
[
self
handleScroll
]
;
}
}
// 松手时还在运动, 先调用scrollViewDidEndDragging,在调用scrollViewDidEndDecelerating
-
(
void
)
scrollViewDidEndDecelerating
:
(
UIScrollView *
)
scrollView
{
// scrollView已经完全静止
[
self
handleScroll
]
;
}
|
做到这里,我们已经能够实现tableView在滑动的时候,流畅的播放视频了。
06、现有的问题(bug)?
很多有经验的老司机,应该已经看出来问题了。问题就是,我们现有的规则是:
- 在可见cell中,播放最靠近屏幕中心的cell的视频。
- 当tableView滑动静止的时候调用视频播放逻辑。
还记得这张图吗?仔细想一下,按照我们上面的规则,cell01是永远不可能在静止的时候成为离屏幕中心最近那个cell的(也有例外,那就是其他三个cell都没有要播放视频。但是,我们的程序不能有设计缺陷)。同样的,底部也有一个这样的cell,不能滚动到屏幕中心。我把这样的cell叫做“滑动不可及cell”,下文都会以这个词来称呼归类这一类cell。
所以,我在上面的两点规则上加了一条:
- 如果“滑动不可及cell”完整出现在视野,那么优先播放“滑动不可及cell”的视频,注意,这里说得是“滑动不可及cell完整出现在视野”,注意点是“完整出现”。
有了规则就依照这个规则来解决。首先,我们面对的第一个问题是,我怎么知道我的列表里有几个这样的cell?不知道,没关系,我们可以实际测量。我以iPhone6s为样机,进行了测量,我这里的测量前提是,我们的cell上的视频尺寸和cell的尺寸是一致的。我的测量结果如下:
1
2
|
每屏
cell个数
4
3
2
滑动不可及
cell个数
1
1
0
|
我这里需要说明的是,我不可能知道你项目的具体需求,但是,如果你的实际需求和我文章中的不一样,那你根据我现有思路进行更改就可以了。
接下来继续,根据以上的分析,我首先把测量结果保存到一个字典中,以每屏可见cell的最大个数为key, 对应的不能播放视频的cell为value。以后,你只要根据行高和屏幕高度这两个值算出每屏有多少cell,就能取出有多少个cell是滑动不可及cell。
1
2
3
4
5
6
7
8
9
10
11
12
|
-
(
NSDictionary *
)
dictOfVisiableAndNotPlayCells
{
// 以每屏可见cell的最大个数为key, 对应的不能播放视频的cell为value
// 只有每屏可见cell数在3以上时,才会出现滑动时有cell的视频永远播放不到
// 以下值都是实际测量得到
if
(
!
_dictOfVisiableAndNotPlayCells
)
{
_dictOfVisiableAndNotPlayCells
=
@
{
@
"4"
:
@
"1"
,
@
"3"
:
@
"1"
,
}
;
}
return
_dictOfVisiableAndNotPlayCells
;
}
|
其次,我把cell归为三个类型,我用一个枚举来表示:
1
2
3
4
5
6
|
// 播放滑动不可及cell的类型
typedef
NS_ENUM
(
NSUInteger
,
PlayUnreachCellStyle
)
{
PlayUnreachCellStyleUp
=
1
,
// 顶部不可及
PlayUnreachCellStyleDown
=
2
,
// 底部不可及
PlayUnreachCellStyleNone
=
3
// 播放滑动可及cell
}
;
|
当每个cell来到cellForRowAtIndexPath方法的时候,我就根据cell的row数和最大不可及cell数,给每个cell打一个标签。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
-
(
UITableViewCell *
)
tableView
:
(
UITableView *
)
tableView
cellForRowAtIndexPath
:
(
NSIndexPath *
)
indexPath
{
JPVideoPlayerCell *
cell
=
[
tableView
dequeueReusableCellWithIdentifier
:
reuseID
forIndexPath
:
indexPath
]
;
if
(
self
.
maxNumCannotPlayVideoCells
>
0
)
{
if
(
indexPath
.
row
=
self
.
listArr
.
count
-
self
.
maxNumCannotPlayVideoCells
)
{
cell
.
cellStyle
=
PlayUnreachCellStyleDown
;
}
else
{
cell
.
cellStyle
=
PlayUnreachCellStyleNone
;
}
}
return
cell
;
}
|
在播放逻辑里加入这些代码,用来维护我上面加的那条规则:
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
|
// 优先查找滑动不可及cell
if
(
cell
.
cellStyle
!=
PlayUnreachCellStyleNone
)
{
// 并且不可及cell要全部露出
if
(
cell
.
cellStyle
==
PlayUnreachCellStyleUp
)
{
CGPoint
cellLeftUpPoint
=
cell
.
frame
.
origin
;
// 不要在边界上
cellLeftUpPoint
.
y
+=
1
;
CGPoint
coorPoint
=
[
cell
.
superview
convertPoint
:
cellLeftUpPoint
toView
:
nil
]
;
CGRect
windowRect
=
self
.
view
.
window
.
bounds
;
BOOL
isContain
=
CGRectContainsPoint
(
windowRect
,
coorPoint
)
;
if
(
isContain
)
{
finnalCell
=
cell
;
}
}
else
if
(
cell
.
cellStyle
==
PlayUnreachCellStyleDown
)
{
CGPoint
cellLeftUpPoint
=
cell
.
frame
.
origin
;
cellLeftUpPoint
.
y
+=
cell
.
bounds
.
size
.
height
;
// 不要在边界上
cellLeftUpPoint
.
y
-=
1
;
CGPoint
coorPoint
=
[
cell
.
superview
convertPoint
:
cellLeftUpPoint
toView
:
nil
]
;
CGRect
windowRect
=
self
.
view
.
window
.
bounds
;
BOOL
isContain
=
CGRectContainsPoint
(
windowRect
,
coorPoint
)
;
if
(
isContain
)
{
finnalCell
=
cell
;
}
}
}
|
到这里为止,我们的播放逻辑基本上没有问题了。
07、真的没有问题了?
其实还是有问题的,就问题就是,当你快速滑动的时候,会出现cell循环利用的图像错误。可以想象,在快速滑动的时候,我们没有做任何处理,上个cell的视频图像在cell移出视野时没有清除,那当这个cell被循环利用的时候,就会把上个cell的图像带到下一个cell,这样就会有显示问题。
其实解决方案很简单,就是当cell移出视野,把对应的图层去掉,播放器释放。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
// 快速滑动循环利用问题
-
(
void
)
handleQuickScroll
{
if
(
!
self
.
playingCell
)
return
;
NSArray *
visiableCells
=
[
self
.
tableView
visibleCells
]
;
NSMutableArray *
indexPaths
=
[
NSMutableArray
array
]
;
for
(
JPVideoPlayerCell *
cell
in
visiableCells
)
{
[
indexPaths
addObject
:
cell
.
indexPath
]
;
}
BOOL
isPlayingCellVisiable
=
YES
;
if
(
!
[
indexPaths
containsObject
:
self
.
playingCell
.
indexPath
]
)
{
isPlayingCellVisiable
=
NO
;
}
// 当前播放视频的cell移出视线, 或者cell被快速的循环利用了, 都要移除播放器
if
(
!
isPlayingCellVisiable
||
!
[
self
.
playingCell
.
videoPath
isEqualToString
:
self
.
currentVideoPath
]
)
{
[
self
stopPlay
]
;
}
}
|
好的,真的没有问题了