2019-05-08 14:08:26 u014174901 阅读数 183

置顶感谢:

https://www.jianshu.com/p/4b15f79cdc33
https://www.jianshu.com/p/75aa645531d2

  首先声明,我对m3u8和ts视频文件和视频播放流媒体相关的知识并不懂。我写这个博客只是为了记录一下我开发中遇到的问题和我的解决思路,如果能帮到你就更好了。

我的需求

  开会时提到,应用中已有直播功能,需要做直播回放。后台初始计划返回ts 文件的完整地址(http://xxxxxxx.ts)列表样式。一头雾水,跳进百度谷歌开始各种查。下面是我查到的我认为有效的信息。

资料查询

  m3u8这个文件可以用你可以下载下来,用txt格式打开看下,基本上文件并不大,打开来看就是一个列表,除去开头的和结尾的一些信息以外,中间部分的意思就是某个ts文件(一般用的都是相对路径)有多久的播放时长,m3u8相当于把这些片段连起来,构成一个完整的视频。

  所以,对于当前项目的需求的话,解决思路就是通过后台提供的ts文件路径表,我在App端创建一个m3u8文件,然后根据m3u8的格式,创建一个字符串写入到m3u8文件中。然后,用播放器去播放本地的m3u8文件。至此,我以为可以,但是并没有播放出来。

尝试方案:

先列出来我的尝试方案:
1.本地创建m3u8文件,文件中写入头尾数据和ts播放列表(相对路径)。 结果:失败(原因:播放器读取这个m3u8 文件之后,根本找不到ts文件,肯定播放失败);
2.本地创建m3u8 文件,文件中写入头尾数据和ts播放列表(完整路径)。结果:失败(具体原因不详);
3.本地创建m3u8 文件,并且把ts文件下载到和m3u8 同一个文件夹下,m3u8 文件中写入头尾数据和ts播放列表(相对路径)。结果:失败(具体原因不详,但是应该和尝试方案2原因类似)。

  大致尝试了以上三种方案,均以失败告终,于是又跳进百度谷歌各种查,发现这篇博客。需要搭载一个本地服务器,我的尝试方案2哥方案3好像就缺少了这个环节。果真,按照这篇博客的介绍,给我的代码稍作改动,视频播放出来了,大功告成。

结论:

  总结:iOS 播放ts文件,需要先根据ts文件名列表生成m3u8文件,(如果m3u8 中放的是ts的相对路径,需要把ts文件下载到和m3u8 同样目录下).在手机本地搭载本地服务,即可播放。方案总结如下:
iOS播放TS文件思路图.png

2019-10-22 23:16:55 A45824430 阅读数 520

WLM3U 是一个用 Swift 实现的 M3U 工具。

项目地址 https://github.com/WillieWangWei/WLM3U

示例

clone 这个仓库,接着执行 pod install 命令,然后运行示例项目。

要求

iOS Swift
9.0 + 5.0 +

安装

WLM3U 可通过 CocoaPods 安装,只需将以下行添加到 Podfile 即可

pod 'WLM3U'

使用

解析 M3U 文件

let url = URL(string:"http://xxx.com/yyy.m3u8")! // M3U 文件的 URL
let size: Int = <#fileSize#>                     // 所有 ts 文件的总大小

WLM3U
    .attach(url: url, size: size, completion: { (result) in
        switch result {
        case .success(let model):
            model.name  // yyy
            model.tsArr // ts 文件数组
            ...
        case .failure(let error):
            print("attach failure " + error.localizedDescription)
        }
    })

下载 M3U 文件描述的 ts 文件

let url = URL(string:"http://xxx.com/yyy.m3u8")! // M3U 文件的 URL
let size: Int = <#fileSize#>                     // 所有 ts 文件的总大小

WLM3U
    .attach(url: url, size: size)
    .download(progress: { (progress, completedCount) in
        progress       // 当前下载的进度
        completedCount // 下载速度( B/S )
        
    }, completion: { (result) in
        switch result {
        case .success(let url):
            url // ts 文件所在的目录
        case .failure(let error):
            print("download failure " + error.localizedDescription)
        }
    })

将下载的 ts 文件合并成一个文件

let url = URL(string:"http://xxx.com/yyy.m3u8")! // M3U 文件的 URL
let size: Int = <#fileSize#>                     // 所有 ts 文件的总大小

WLM3U
    .attach(url: url, size: size)
    .download()
    .combine(completion: { (result) in
        switch result {
        case .success(let url):
            url // 合并完成后文件所在的目录
        case .failure(let error):
            print("combine failure " + error.localizedDescription)
        }
    })

自动获取 ts 文件总大小

WLM3U 支持自动获取所有文件的总大小,只需设置 calculateSize 参数即可:

let url = URL(string:"http://xxx.com/yyy.m3u8")! // M3U 文件的 URL

WLM3U
    .attach(url: url, calculateSize: true)
    .download()
    .combine()

获取大小的过程是异步的,可以通过接收 TaskGetFileSizeProgressNotificationTaskGetFileSizeCompletionNotification 来获取大小数据。

暂停与恢复任务

为了简化接口,WLM3U 没有 暂停恢复 的概念,它们和 取消添加 是一样的,所以:

需要暂停一个任务时,调用 cancel(url: URL)

需要取消一个任务时,调用 cancel(url: URL),并通过 folder(for url: URL) 获取到此任务缓存目录,并删除它即可。

需要添加一个任务时,调用 attach(url: URL)

需要恢复一个任务时,调用 attach(url: URL),如果本地存在之前的缓存,会自动继续下载剩余的文件。

监听状态

WLM3U 内置了几个状态的通知,你可以接收这些通知来处理数据:

/// 下载进度发生变化时会发出的通知。
public let TaskProgressNotification: Notification.Name

/// 获取文件总大小的进度发生变化时会发出的通知。
public let TaskGetFileSizeProgressNotification: Notification.Name

/// 获取文件总大小完成时会发出的通知。
public let TaskGetFileSizeCompletionNotification: Notification.Name

/// 任务完成时会发出的通知。
public let TaskCompletionNotification: Notification.Name

/// 任务发生错误时会发出的通知。
public let TaskErrorNotification: Notification.Name

播放下载的文件

AVPlayer 与 WLM3U 暂不支持播放本地 ts 文件,这里提供两个简单可行的替代方案。

使用 GCDWebServer 搭建本地服务

引入 GCDWebServer 库:

pod "GCDWebServer"

创建本地 HTTP 服务来提供下载好的 ts 文件:

let server = GCDWebServer()
let path = <#folderPath#> // ts 文件所在的本地目录
server.addGETHandler(forBasePath: "/",
                     directoryPath: path,
                     indexFilename: "file.m3u8",
                     cacheAge: 3600,
                     allowRangeRequests: true)
server.start()

使用 AVPlayer 来播放本地服务提供的 ts 文件:

let url = URL(string: "http://localhost:\(server.port)/file.m3u8")
let player = AVPlayer(url: url)

使用 FFmpeg 将 ts 文件转码成 mp4 文件

引入 mobile-ffmpeg-full 库:

pod "mobile-ffmpeg-full"

执行转码命令:

let command = "-i 'ts文件所在的路径' 'mp4文件要保存到的路径'"

let result = MobileFFmpeg.execute(command)

if result == RETURN_CODE_SUCCESS {
    // 转码完成
}

接下来直接播放转码得到的 mp4 文件即可。

作者

Willie, willie.wangwei@gmail.com

2020-03-31 13:41:51 EasyNVR 阅读数 30

今天我打算跟大家聊一点硬技巧,比如怎么使用ffmpeg监测.m3u8直播视频流的状态。现在就来举个例子,已知一个http://xxxxxx.m3u8的直播视频流,需要通过使用ffmpeg监测该直播视频流现在的状态,我们该如何实现呢?

1.安装ffmpeg

这里拿IOS的系统举例子,在mac上安装ffmpeg使用Homebrew,打开终端输入命令:brew install ffmpeg,安装的是最新版本v3.3.2。(在Linux上可以使用yum或者apt-get进行安装)

2.使用ffmpeg命令监测当前直播视频流的状态

通过使用ffmpeg命令将直播视频流实时保存至本地,从而可以监测到当前直播视频流的状态。在终端运行

ffmpeg -i http://xxxxxx.m3u8 -c copy out.mp4

-i 设定输入流

-c 设置编码器。当为copy指复制当前视频的编码流

当没有直播视频流时,检测系统会显示HTTP error 404 Not Found

 

当正在直播时:该命令会把直播视频流分段下载值本地并合并保存

 

当直播关闭或者中断:该命令会中断执行,输出视频out.mp4至你的当前用户的文件夹(/Users/bingmax/out.mp4),并提示HTTP error 404 Not Found

 

这样ffmpeg 监测系统就完成并且奏效了。当然将视频流推送到我们的流媒体服务器,也是可以直接观测视频流的状态,并且可以做实时的监控和调整。

 

2017-10-12 10:46:16 weixin_34320946 阅读数 4532

一、m3u8缓存播放的整个流程

1.下载m3u8文件
2.解析m3u8文件获取视频切片单元的信息。
3.根据2.获取的视频切片信息中的切片链接下载切片并保持到本地。
3.根据获取的切片信息与本地服务器的配置信息,拼接出切片的本地地址、生成新的m3u8文件并保存到本地。
4.开启本地服务器,使用本地url播放本地m3u8文件。

附上:时序图,具体可看demo

Created with Raphaël 2.1.0MasterMasterZBLM3u8ManagerZBLM3u8ManagerZBLM3u8DownloadContainerZBLM3u8DownloadContainerZBLM3u8AnalysiserZBLM3u8AnalysiserZBLM3u8DownloaderZBLM3u8DownloaderNetworkNetwork使用这个url下载视频使用这个url下载视频创建本地目录根据url获取m3u8信息并返回解析信息返回解析的m3u8信息组装下载的ts文件信息(包括key)下载ts文件发起下载返回ts文件数据保存ts文件全部下载成功请组装新的m3u8信息返回新的m3u8信息根据新的m3u8信息保存m3u8文件下载成功下载成功并返回播放链接

二、控制媒体下载的并发数

       这里使用信号量来控制并发数

- (void)downloadVideoWithUrlString:(NSString *)urlStr downloadProgressHandler:(ZBLM3u8ManagerDownloadProgressHandler)downloadProgressHandler downloadSuccessBlock:(ZBLM3u8ManagerDownloadSuccessBlock) downloadSuccessBlock
{
    dispatch_async(_downloadQueue, ^{
        dispatch_semaphore_wait(_movieSemaphore, DISPATCH_TIME_FOREVER);
        __weak __typeof(self) weakself = self;
        [[self downloadContainerWithUrlString:urlStr] startDownloadWithUrlString:urlStr  downloadProgressHandler:^(float progress) {
            downloadProgressHandler(progress);
        } completaionHandler:^(NSString *locaLUrl, NSError *error) {
            if (!error) {
                [weakself.downloadContainerDictionary removeObjectForKey:[ZBLM3u8Setting uuidWithUrl:urlStr]];
                NSLog(@"下载完成:%@",urlStr);
                downloadSuccessBlock(locaLUrl);
            }
            else
            {
                NSLog(@"下载失败:%@",error);
                [self resumeDownload];
            }
            NSLog(@"%@",weakself.downloadContainerDictionary.allKeys);
            dispatch_semaphore_signal(_movieSemaphore);
        }];
    });
}

这里可以设置_movieSemaphore的的初始值为具体的可同时下载数。
Example:_movieSemaphore = dispatch_semaphore_create(1),意味着同一时间只允许下载一个视频,等同于视频的串行下载。

三、控制单个媒体的切片下载并发数

       开始的时候,考虑使用AFURLSessionManager中的operationQueue.maxConcurrentOperationCount来控制并发。但这是行不通的。因为这个queue是用于回调而不是用于下载队列。

/**
 The operation queue on which delegate callbacks are run.
 */
@property (readonly, nonatomic, strong) NSOperationQueue *operationQueue;

再看AF中初始化

- (instancetype)initWithSessionConfiguration:(NSURLSessionConfiguration *)configuration {
    self = [super init];
    if (!self) {
        return nil;
    }

    if (!configuration) {
        configuration = [NSURLSessionConfiguration defaultSessionConfiguration];
    }

    self.sessionConfiguration = configuration;

    self.operationQueue = [[NSOperationQueue alloc] init];
    self.operationQueue.maxConcurrentOperationCount = 1;

    self.session = [NSURLSession sessionWithConfiguration:self.sessionConfiguration delegate:self delegateQueue:self.operationQueue];
...

       这个queue确实是用于回调,而我是需要控制下载并发。这似乎不满足。而且实测中也发现确实不行。那么,只能在任务发起哪里做并发控制,同样,这里还是采用信号量。这里的控制相对复杂一点、因为后面的任务恢复、失败任务重新创建也要做控制。

- (void)startDownload
{
    //因为这是外部调用的方法,操作的执行要放到异步线程中。避免因为并发控制中的等待而堵塞外部线程
    dispatch_async(self.downloadQueue, ^{
        if (!_fileDownloadInfos.count) {
            _completaionHandler(nil);
            return;
        }
        NSLog(@"downloadInfoCount:%ld",(long)_fileDownloadInfos.count);

        [_fileDownloadInfos enumerateObjectsUsingBlock:^(ZBLM3u8FileDownloadInfo * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
            //控制切片下载并发
            dispatch_semaphore_wait(self.tsSemaphore, DISPATCH_TIME_FOREVER);
            if ([ZBLM3u8FileManager exitItemWithPath:obj.filePath]) {
                obj.success = YES;
                [self verifyDownloadCountAndCallbackByDownloadSuccess:YES];
            }
            else
            {
                //如果收到中断信号,中断下载流程释放信号量并返回
                if (self.suspend) {
                    obj.beStopCreateTask = YES;
                    dispatch_semaphore_signal(self.tsSemaphore);
                    NSLog(@"suspend and return! don not createDownloadTask!");
                    return ;
                }
                else
                {
                    //真正的创建下载任务
                    [self createDownloadTaskWithIndex:idx];
                }
            }
        }];
    });
}

//信号量在每个任务的回调后都会释放一次
- (void)verifyDownloadCountAndCallbackByDownloadSuccess:(BOOL) isSuccess
{
    dispatch_semaphore_signal(self.tsSemaphore);
...
  • 这里也看到信号量的控制问题,必须理清信号量的获得和释放时机,一次获得必须有一次释放。不释放或者重复释放,都会导致并发的控制不准。如果这样,那这里的并发控制就没有意义了。

  • 要做到准确获取和释放,重点在于理清程序的执行路径。在每一条执行路径中都必须释放信号量。这个跟锁的使用也是一样的。

  • 调试现象:如果程序不像预料中运行,又没有什么错误,那很有可能就是堵塞了。锁没有释放或者信号量的处理有问题。
    处理步骤:点击xcode调试栏的暂停按钮,查看程序的调用栈,分析每个线程的运行情况,找到堵塞具体执行代码。根据具体的逻辑修正问题。

四、下载的中断和恢复

       这里有几个小问题:根据判断NSURLSessionTask 提供的3个方法可以做一些中断和恢复处理
- (void)suspend;
       挂起任务,但只能挂起执行中的任务。对于已经创建而且执行resum方法但并没真正执行的任务无效(这里非常坑)。
通常我们使用这个方法的时候会判断下任务的具体状态,如果是task.state == NSURLSessionTaskStateRunning采取执行 [task suspend]。但这个判断是不准确的。如果一个任务创建并执行resume但并没真正执行,它的状态也是为NSURLSessionTaskStateRunning。如果这个时候程序收到中断消息,对状态为NSURLSessionTaskStateRunning 的任务全部执行suspend操作,你会发现有些任务不听话,继续执行。到底什么搞鬼…
我的理解是这样的,这些不听话的任务正是那些添加到下载队列中等待执行的任务,而在等待状态下收到suspend消息是不管用的。但它接收cannel消息是管用的。那么问题的解决就是找出这些等待的任务。
处理办法:通过判断接收字节数来区分状态。现在我只面向你接收的字节数,而不管你真开启还是假开启了。

switch (obj.downloadTask.state) {
                case NSURLSessionTaskStateRunning:
                {
                    //等待中,假开启状态,
                    if (obj.downloadTask.countOfBytesReceived <= 0) {
                        [obj.downloadTask cancel];
                    }
                    else
                    {
                    //正在下载,真开启状态,
                        [obj.downloadTask suspend];
                    }
                }
                    break;
  • (void)resume;
    官方文档是这么说明的:Resumes the task, if it is suspended.意思是指只能发起被挂起的任务。
    存在两种情况:
     1.新创建的任务并没有执行resume,此时状态为:NSURLSessionTaskStateSuspended
     2.执行suspend方法后被手动挂起的任务,状态同为NSURLSessionTaskStateSuspended。
       同时这里提供了额外信息:状态为NSURLSessionTaskStateCompleted的任务是不能通过resume重新发起的。而在某些情况下我们需要对这种状态的任务重新发起,包括手动cannel的、执行失败的。这种情况下只能根据具体的情况,重新创建任务并发起。
if (obj.downloadTask.error && 
    obj.downloadTask.state == NSURLSessionTaskStateCompleted)
{
              //下载失败的任务重新创建
                [self createDownloadTaskWithIndex:idx];
 }
  • (void)cancel;
    官方文档说明:

    This method returns immediately, marking the task as being canceled. Once a task is marked as being canceled, URLSession:task:didCompleteWithError: will be sent to the task delegate, passing an error in the domain NSURLErrorDomain with the code NSURLErrorCancelled. A task may, under some circumstances, send messages to its delegate before the cancelation is acknowledged.
    This method may be called on a task that is suspended.

    简而言之:调用这个方法可以cannel任意状态的任务包括挂起的任务。并执行didCompleteWithError回调(AF中会执行回调并返回错误NSURLErrorCancelled),状态被标记为NSURLSessionTaskStateCompleted。故上面恢复失败任务的时候,通过task.state和task.error共同判断。

总结下任务生命周期中的任务状态变化:

1.创建成功:…Suspended
2.执行resume:…Running(可以通过判断countOfBytesReceived来区分任务处于等待还是下载中)
3.执行suspend:…Suspended
4.执行cannel:(中间状态…Canceling)->…Completed(可以结合task.error判断任务执行结果)

回归正题:
    视频单元的中断和恢复,中断就是调用suspend方法挂起正在执行的任务,cannel掉等待的任务,代码跟上面说明方法的时候非常雷同。这里着重说明下恢复。如果单单恢复其实很简单,恢复挂起的任务和重新创建错误的任务。这只是从程序的角度看待,要使一个程序有更高的可用性,应在功能实现的同时做的更加的合理。应优先恢复挂起的任务、然后重新创建错误的任务。这样做都是为了承前启后更快的把一个下载任务完成。而且这个视频切片是讲究有序的,所以我们恢复的时候也要遵从FIFO的原则。

五、注意的问题与思路延伸

1.解析m3u8注意的问题
      这里的解析格式太多,很容易出现问题,应使用try/catch来保证程序的健壮性。

2.根据url获取原始m3u8文件信息,这个操作太耗时了。为了提高程序的效率,获取到原始m3u8文件后应做本地持久化并用于二次下载。如果整个流程下载成功,可以选择删除该文件,或者不操作。由于是纯文本文件,少量的文件冗余是允许的。

3.文件的操作通过开启一个同步队列来处。可以设定low优先级避免占用太高的cpu资源。其实高cpu占用会伴随着另外一个问题,手机的发热量。

4.key的处理问题
      如果存在key的下载,需要把key下载到本地,约定好key的名称和新建m3u8文件中的key链接。这样本地播放就能正常加解密。
例如下载到本地的key保存为…/key。那么链接应该是http:localhost:port/…/key

5.中断的优先级
      程序的中断操作拥有最高优先级的,因为要任何状态下都能中断下载。无论是为了程序的流畅性、网络变为移动信号避免使用用户的移动流量等发出中断命令,都必须立即响应。

6.切片数量的全局分配
      多个视频同时下载,多个切片同时并发。如果要做到控制全局的切片并发数而不是单个视频的切片并发数。这就要设计一个算法在全局Manger哪里做分配和回收。

7.保证app流程,监控网速开启和中断下载
      下载视频的功能应该要保证app本身网络请求的正常运行。
app如何获取到网络的带宽,好像只能通过下载文件方式来推算。可在应用请求空闲时通过短时间下载一个可用源来计算带宽,同时监控app 实时网络吞吐,适当的开关下载。虽然很难做到实时,但是在切换网络的时候进行带宽重测、又或者地理位置变化一定距离后进行带宽重测、又或者定时作带宽重测。还有就是考虑wifi状态下才进行下载。

8.切换网络后请求失去连接,恢复下载的问题
      因为网络切换本地ip变化,发起的请求会失去连接。如果通过downloadTask是没办法做到恢复下载的。虽然可以用resumeData来恢复下载,但是这个只能在cannel操作的时候获取,至于失去连接的情况下是没办法获取到的(系统提供的api中没有在失败回调哪里返回resumeData的)。这个时候需要自己创建文件句柄,使用dataTask做到文件续下。初始化dataTask的时候设定请求头’Accept-Ranges’参数为文件的已下载字节数(需要服务器支持),就可以获取到未下载的部分数据。

9.并发中锁的处理
      要理清那些代码可能存在并发,那些操作要保证原子性。难就难在一个方法中会存在部分代码块是并发执行的,这有利于效率的提高;部分些代码要原子操作。最优的做法就是对原子操作用锁来保证,没有任何多余的代码加入到同步操作中,这样也是效率最高的。而拿捏不准的情况下,可以锁定更多的代码,至少这样不会因为并发而导致问题,但这样就牺牲了效率和及时性。当一个简单的系统,要做到最优好像并不难,但是一个复杂的系统做到最优就非常难了或者是要花费非常大的精力。

10.是否需要全部切片下载完成才能播放
      其实并不需要全部下载完成就能播放的。一个车子开起来只要保证后面的能源供应充足就能一直正常运作。首先保证key 先下载下来,而且要保证有序下载,然后下载一定量的切片文件,这个时候就可以组装m3u8文件到本地,发起播放。只要后面下载的切片能满足播放器的播放,就不会出现问题。但如果供应不足视频就会停了,播放不了,尽管后面文件下载下来了,还是不能自动恢复,仿佛失去了缓冲功能。这里就是跟直接请求服务器的差别了,直接请求服务器,因为文件本身是存在的,发起的请求是存在的,如果网速慢,播放器的反应是缓冲;而本地服务播放就不同了,如果文件在播放前没有下载下来,发起的请求立马就挂了,这个请求不存在,当然就不存在缓冲。

简书地址:http://www.jianshu.com/p/c0a78d8ed231
dome中应该有很多不足之处,请多多赐教。
链接:https://github.com/zmubai/ZBLM3U8DownLoadTest

2014-01-09 15:26:32 prevention 阅读数 8846

iOS Dev (24) 最简单的M3U8播放器

概述

用 MediaPlayer Framework 中的 MPMoviePlayerController 构造一个最简单的 M3U8 播放器。

Show Me the Codes

创建一个空项目,然后改写 AppDelegate:

AppDelegate.h

#import <UIKit/UIKit.h>

@class PlayerViewController;

@interface AppDelegate : UIResponder <UIApplicationDelegate>

@property (strong, nonatomic) UIWindow *window;
@property (strong, nonatomic) PlayerViewController *vc;

@end

AppDelegate.m

#import "AppDelegate.h"
#import "PlayerViewController.h"

@implementation AppDelegate

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary    *)launchOptions
{
    self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
    self.vc = [[PlayerViewController alloc] initWithNibName:nil bundle:nil];
    self.window.rootViewController = self.vc;
    [self.window makeKeyAndVisible];
    return YES;
}

...

@end

其他默认函数我就略了。

PlayerViewController.h

#import <UIKit/UIKit.h>

@interface PlayerViewController : UIViewController

@end

PlayerViewController.m

在 viewDidLoad 中初始化 MPMoviePlayerController,并指定一个播放地址。这里我写死了地址,是为了演示。

#import "PlayerViewController.h"
#import <MediaPlayer/MediaPlayer.h>

@interface PlayerViewController ()

@property (strong, nonatomic) MPMoviePlayerController *streamPlayer;

@end

@implementation PlayerViewController

- (void)viewDidLoad
{
    [super viewDidLoad];

    NSURL *streamURL = [NSURL URLWithString:@"http://www.thumbafon.com/code_examples/video/segment_example/prog_index.m3u8"];

    self.streamPlayer = [[MPMoviePlayerController alloc] initWithContentURL:streamURL];
    [self.streamPlayer.view setFrame:self.view.bounds];
    self.streamPlayer.controlStyle = MPMovieControlStyleEmbedded;

    [self.view addSubview: self.streamPlayer.view];
    [self.streamPlayer play];
}

- (void)didReceiveMemoryWarning
{
    [super didReceiveMemoryWarning];
    // Dispose of any resources that can be recreated.
}

@end

Run It !

-

转载请注明来自:http://blog.csdn.net/prevention

没有更多推荐了,返回首页