iOS視頻錄制壓縮與上傳

我們?cè)陧?xiàng)目中有時(shí)會(huì)碰到視頻相關(guān)的需求耐亏,一般的可以分為幾種情況:

1. 簡(jiǎn)單的視頻開(kāi)發(fā),對(duì)界面無(wú)要求,可直接使用系統(tǒng)UIImagePickerController洗出。

(1)使用UIImagePickerController視頻錄制,短視頻10秒鐘

(2)在UIImagePickerController代理方法 didFinishPickingMediaWithInfo图谷,使用AVAssetExportSession轉(zhuǎn)碼MP4(一般要兼容Android播放翩活,iOS默認(rèn)是mov格式)

(3)使用AFNetWorking上傳到服務(wù)器

(4)網(wǎng)絡(luò)請(qǐng)求,使用AVPlayerViewControlller在線播放視頻流便贵。(在iOS9之后,蘋(píng)果已經(jīng)棄用MPMoviePlayerController)

1.1 UIImagePickerController

UIImagePickerController繼承于UINavigationController菠镇。UIImagePickerController可以用來(lái)選擇照片,它還可以用來(lái)拍照和錄制視頻承璃。

  1. sourceType: 拾取源類型利耍,sourceType是枚舉類型:

    UIImagePickerControllerSourceTypePhotoLibrary:照片庫(kù),默認(rèn)值
    UIImagePickerControllerSourceTypeCamera:攝像頭
    UIImagePickerControllerSourceTypeSavedPhotosAlbum:相簿

  2. mediaTypes:媒體類型,默認(rèn)情況下此數(shù)組包含kUTTypeImage,所以拍照時(shí)可以不用設(shè)置隘梨;但是當(dāng)要錄像的時(shí)候必須設(shè)置程癌,可以設(shè)置為kUTTypeVideo(視頻,但不帶聲音)或者kUTTypeMovie(視頻并帶有聲音)

  3. videoMaximumDuration:視頻最大錄制時(shí)長(zhǎng)轴猎,默認(rèn)為10 s

  4. videoQuality:視頻質(zhì)量嵌莉,枚舉類型:
    UIImagePickerControllerQualityTypeHigh:高清質(zhì)量
    UIImagePickerControllerQualityTypeMedium:中等質(zhì)量,適合WiFi傳輸
    UIImagePickerControllerQualityTypeLow:低質(zhì)量捻脖,適合蜂窩網(wǎng)傳輸
    UIImagePickerControllerQualityType640x480:640480
    UIImagePickerControllerQualityTypeIFrame1280x720:1280
    720
    UIImagePickerControllerQualityTypeIFrame960x540:960*540

  5. cameraDevice:攝像頭設(shè)備锐峭,cameraDevice是枚舉類型:
    UIImagePickerControllerCameraDeviceRear:前置攝像頭
    UIImagePickerControllerCameraDeviceFront:后置攝像頭

  6. cameraFlashMode:閃光燈模式,枚舉類型:UIImagePickerControllerCameraFlashModeOff:關(guān)閉閃光燈UIImagePickerControllerCameraFlashModeAuto:閃光燈自動(dòng)UIImagePickerControllerCameraFlashModeOn:打開(kāi)閃光燈

  7. (BOOL)isSourceTypeAvailable:(UIImagePickerControllerSourceType)sourceType指定的源類型是否可用可婶,sourceType是枚舉類型:
    UIImagePickerControllerSourceTypePhotoLibrary:照片庫(kù)
    UIImagePickerControllerSourceTypeCamera:攝像頭
    UIImagePickerControllerSourceTypeSavedPhotosAlbum:相簿

  8. UIImageWriteToSavedPhotosAlbum(UIImage *image, id completionTarget, SEL completionSelector, void *contextInfo)   //保存照片到相簿
    
  9. UISaveVideoAtPathToSavedPhotosAlbum(NSString *videoPath, id completionTarget, SEL completionSelector, void *contextInfo) //保存視頻到相簿
    

    示例:

        //使用UIImagePickerController視頻錄制
        UIImagePickerController *picker = [[UIImagePickerController alloc] init];
        picker.delegate = self;
        if ([UIImagePickerController isSourceTypeAvailable:UIImagePickerControllerSourceTypeCamera]) {
            picker.sourceType = UIImagePickerControllerSourceTypeCamera;
        }
        
        //mediaTypes設(shè)置攝影還是拍照
        //kUTTypeImage 對(duì)應(yīng)拍照
        //kUTTypeMovie  對(duì)應(yīng)攝像
    //    NSString *requiredMediaType = ( NSString *)kUTTypeImage;
        NSString *requiredMediaType1 = ( NSString *)kUTTypeMovie;
        NSArray *arrMediaTypes=[NSArray arrayWithObjects:requiredMediaType1,nil];
        picker.mediaTypes = arrMediaTypes;
    //    picker.videoQuality = UIImagePickerControllerQualityTypeHigh;默認(rèn)是中等
        picker.videoMaximumDuration = 60.; //60秒
        [self presentViewController:picker animated:YES completion:^{
            
        }];
    

    ?

1.2 AVAsset

AVAsset是一個(gè)抽象類和不可變類沿癞。定義了媒體資源混合呈現(xiàn)的方式。主要用于獲取多媒體信息矛渴,既然是一個(gè)抽象類椎扬,那么當(dāng)然不能直接使用∈镄瘢可以讓開(kāi)發(fā)者在處理流媒體時(shí)提供一種簡(jiǎn)單統(tǒng)一的方式盗舰,它并不是媒體資源,但是它可以作為時(shí)基媒體的容器。

實(shí)際上是創(chuàng)建的是它的子類AVUrlAsset的一個(gè)實(shí)例桂躏。通過(guò)AVUrlAsset我們可以創(chuàng)建一個(gè)帶選項(xiàng)(optional)的asset钻趋,以提供更精確的時(shí)長(zhǎng)和計(jì)時(shí)信息。比如通過(guò)AVURLAsset的duration計(jì)算視頻時(shí)長(zhǎng)剂习。

/**
 獲取視頻時(shí)長(zhǎng)

 @param url url
 @return s
 */
- (CGFloat)getVideoLength:(NSURL *)url{
    AVURLAsset *avUrl = [AVURLAsset assetWithURL:url];
    CMTime time = [avUrl duration];
    int second = ceil(time.value/time.timescale);
    return second;
}

1.3 AVAssetExportSession

AVAssetExportSession是用于為AVAsset源對(duì)象進(jìn)行轉(zhuǎn)碼輸出蛮位,可進(jìn)行格式轉(zhuǎn)碼,壓縮等鳞绕。

 AVURLAsset *avAsset = [AVURLAsset URLAssetWithURL:inputURL options:nil];
    
    AVAssetExportSession *exportSession = [[AVAssetExportSession alloc] initWithAsset:avAsset presetName:AVAssetExportPresetMediumQuality];

    exportSession.outputURL = outputURL;
    exportSession.outputFileType = AVFileTypeMPEG4;
    exportSession.shouldOptimizeForNetworkUse= YES;
    [exportSession exportAsynchronouslyWithCompletionHandler:nil];

1.4 AVAssetImageGenerator

AVAssetImageGenerator能夠取出視頻中每一幀的縮略圖或者預(yù)覽圖失仁。舉個(gè)??:

  // result
    UIImage *image = nil;
    
    // AVAssetImageGenerator
    AVAsset *asset = [AVAsset assetWithURL:videoURL];
    AVAssetImageGenerator *imageGenerator = [[AVAssetImageGenerator alloc] initWithAsset:asset];
    imageGenerator.appliesPreferredTrackTransform = YES;
    
    // calculate the midpoint time of video
    Float64 duration = CMTimeGetSeconds([asset duration]);
    // 取某個(gè)幀的時(shí)間,參數(shù)一表示哪個(gè)時(shí)間(秒)们何,參數(shù)二表示每秒多少幀
    // 通常來(lái)說(shuō)萄焦,600是一個(gè)常用的公共參數(shù),蘋(píng)果有說(shuō)明:
    // 24 frames per second (fps) for film, 30 fps for NTSC (used for TV in North America and
    // Japan), and 25 fps for PAL (used for TV in Europe).
    // Using a timescale of 600, you can exactly represent any number of frames in these systems
    CMTime midpoint = CMTimeMakeWithSeconds(duration / 2.0, 600);
    
    // get the image from
    NSError *error = nil;
    CMTime actualTime;
    // Returns a CFRetained CGImageRef for an asset at or near the specified time.
    // So we should mannully release it
    CGImageRef centerFrameImage = [imageGenerator copyCGImageAtTime:midpoint
                                                         actualTime:&actualTime
                                                              error:&error];
    
    if (centerFrameImage != NULL) {
        image = [[UIImage alloc] initWithCGImage:centerFrameImage];
        // Release the CFRetained image
        CGImageRelease(centerFrameImage);
    }
    
    return image;

1.5 AVPlayerViewControlller

AVPlayerViewController是UIViewController的子類,它可以用來(lái)顯示AVPlayer對(duì)象的視覺(jué)內(nèi)容和標(biāo)準(zhǔn)的播放控制冤竹。

具體AVPlayer自定義下面再說(shuō)拂封,回到AVPlayerViewController,它不支持UI自定義鹦蠕,實(shí)現(xiàn)比較簡(jiǎn)單冒签,代碼如下:

    AVPlayer *player = [AVPlayer playerWithURL:self.finalURL];
    AVPlayerViewController *playerVC = [[AVPlayerViewController alloc] init];
    playerVC.player = player;
    playerVC.view.frame = self.view.frame;
    [self.view addSubview:playerVC.view];
    [playerVC.player play];


2. 復(fù)雜的業(yè)務(wù)需求,需要自定義視頻界面

(1)使用AVFoundation拍照和錄制視頻钟病,自定義界面

(2)使用AVAssetExportSession轉(zhuǎn)碼MP4(一般要兼容Android播放萧恕,iOS默認(rèn)是mov格式)

(3)使用AFNetWorking上傳到服務(wù)器

(4)網(wǎng)絡(luò)請(qǐng)求刚梭,使用AVFoundation框架的AVPlayer來(lái)自定義播放界面,在線播放視頻流票唆。播放又分為先下后播和邊下邊播朴读。

2.1 AVFoundation

AVFoundation是一個(gè)可以用來(lái)使用和創(chuàng)建基于時(shí)間的視聽(tīng)媒體的框架,它提供了一個(gè)能使用基于時(shí)間的視聽(tīng)數(shù)據(jù)的詳細(xì)級(jí)別的Objective-C接口惰说。例如:您可以用它來(lái)檢查磨德,創(chuàng)建缘回,編輯或是重新編碼媒體文件吆视。也可以從設(shè)備中獲取輸入流,在視頻實(shí)時(shí)播放時(shí)操作和回放酥宴。

avfoundation.png

AVCaptureSession:媒體(音啦吧、視頻)捕獲會(huì)話,負(fù)責(zé)把捕獲的音視頻數(shù)據(jù)輸出到輸出設(shè)備中拙寡。一個(gè)AVCaptureSession可以有多個(gè)輸入輸出授滓。
AVCaptureDevice :輸入設(shè)備,包括麥克風(fēng)肆糕、攝像頭般堆,通過(guò)該對(duì)象可以設(shè)置物理設(shè)備的一些屬性(例如相機(jī)聚焦、白平衡等)诚啃。
AVCaptureDeviceInput :設(shè)備輸入數(shù)據(jù)管理對(duì)象淮摔,可以根據(jù)AVCaptureDevice創(chuàng)建對(duì)應(yīng)的AVCaptureDeviceInput對(duì)象,該對(duì)象將會(huì)被添加到AVCaptureSession中管理始赎。
AVCaptureVideoPreviewLayer :相機(jī)拍攝預(yù)覽圖層和橙,是CALayer的子類,使用該對(duì)象可以實(shí)時(shí)查看拍照或視頻錄制效果造垛,創(chuàng)建該對(duì)象需要指定對(duì)應(yīng)的 AVCaptureSession對(duì)象魔招。

AVCaptureOutput :輸出數(shù)據(jù)管理對(duì)象,用于接收各類輸出數(shù)據(jù)五辽,通常使用對(duì)應(yīng)的子類AVCaptureAudioDataOutput办斑、AVCaptureStillImageOutput、AVCaptureVideoDataOutput杆逗、AVCaptureFileOutput, 該對(duì)象將會(huì)被添加到AVCaptureSession中管理乡翅。

relationship.png

建立視頻拍攝的步驟如下:

  1. 創(chuàng)建AVCaptureSession對(duì)象。
// 創(chuàng)建會(huì)話 (AVCaptureSession) 對(duì)象髓迎。
_captureSession = [[AVCaptureSession alloc] init];
if ([_captureSession canSetSessionPreset:AVCaptureSessionPreset640x480]) {
    // 設(shè)置會(huì)話的 sessionPreset 屬性, 這個(gè)屬性影響視頻的分辨率
    [_captureSession setSessionPreset:AVCaptureSessionPreset640x480];
}
  1. 使用AVCaptureDevice的靜態(tài)方法獲得需要使用的設(shè)備峦朗,例如拍照和錄像就需要獲得攝像頭設(shè)備,錄音就要獲得麥克風(fēng)設(shè)備排龄。
// 獲取攝像頭輸入設(shè)備波势, 創(chuàng)建 AVCaptureDeviceInput 對(duì)象
// 在獲取攝像頭的時(shí)候翎朱,攝像頭分為前后攝像頭,我們創(chuàng)建了一個(gè)方法通過(guò)用攝像頭的位置來(lái)獲取攝像頭 
AVCaptureDevice *videoCaptureDevice = [self getCameraDeviceWithPosition:AVCaptureDevicePositionBack];
if (!captureDevice) {
    NSLog(@"---- 取得后置攝像頭時(shí)出現(xiàn)問(wèn)題---- ");
    return;
}

// 添加一個(gè)音頻輸入設(shè)備
// 直接可以拿數(shù)組中的數(shù)組中的第一個(gè)
AVCaptureDevice *audioCaptureDevice = [[AVCaptureDevice devicesWithMediaType:AVMediaTypeAudio] firstObject];
  1. 利用輸入設(shè)備AVCaptureDevice初始化AVCaptureDeviceInput對(duì)象尺铣。
// 視頻輸入對(duì)象
// 根據(jù)輸入設(shè)備初始化輸入對(duì)象拴曲,用戶獲取輸入數(shù)據(jù)
_videoCaptureDeviceInput = [[AVCaptureDeviceInput alloc] initWithDevice:captureDevice error:&error];
if (error) {
    NSLog(@"---- 取得設(shè)備輸入對(duì)象時(shí)出錯(cuò) ------ %@",error);
    return;
} 

//  音頻輸入對(duì)象
//根據(jù)輸入設(shè)備初始化設(shè)備輸入對(duì)象,用于獲得輸入數(shù)據(jù)
_audioCaptureDeviceInput = [[AVCaptureDeviceInput alloc] initWithDevice:audioCaptureDevice error:&error];
if (error) {
    NSLog(@"取得設(shè)備輸入對(duì)象時(shí)出錯(cuò) ------ %@",error);
    return;
}
  1. 初始化輸出數(shù)據(jù)管理對(duì)象凛忿,如果要拍照就初始化AVCaptureStillImageOutput對(duì)象澈灼;如果拍攝視頻就初始化AVCaptureMovieFileOutput對(duì)象。
// 拍攝視頻輸出對(duì)象
// 初始化輸出設(shè)備對(duì)象店溢,用戶獲取輸出數(shù)據(jù)
_caputureMovieFileOutput = [[AVCaptureMovieFileOutput alloc] init];
  1. 將數(shù)據(jù)輸入對(duì)象AVCaptureDeviceInput叁熔、數(shù)據(jù)輸出對(duì)象AVCaptureOutput添加到媒體會(huì)話管理對(duì)象AVCaptureSession中。
// 將視頻輸入對(duì)象添加到會(huì)話 (AVCaptureSession) 中
if ([_captureSession canAddInput:_videoCaptureDeviceInput]) {
    [_captureSession addInput:_videoCaptureDeviceInput];
}

// 將音頻輸入對(duì)象添加到會(huì)話 (AVCaptureSession) 中
if ([_captureSession canAddInput:_captureDeviceInput]) {
    [_captureSession addInput:audioCaptureDeviceInput];
    AVCaptureConnection *captureConnection = [_caputureMovieFileOutput connectionWithMediaType:AVMediaTypeVideo];
    // 標(biāo)識(shí)視頻錄入時(shí)穩(wěn)定音頻流的接受床牧,我們這里設(shè)置為自動(dòng)
    if ([captureConnection isVideoStabilizationSupported]) {
        captureConnection.preferredVideoStabilizationMode = AVCaptureVideoStabilizationModeAuto;
    }
}
  1. 創(chuàng)建視頻預(yù)覽圖層AVCaptureVideoPreviewLayer并指定媒體會(huì)話荣回,添加圖層到顯示容器中,調(diào)用AVCaptureSession的startRuning方法開(kāi)始捕獲戈咳。
// 通過(guò)會(huì)話 (AVCaptureSession) 創(chuàng)建預(yù)覽層
_captureVideoPreviewLayer = [[AVCaptureVideoPreviewLayer alloc] initWithSession:_captureSession];

// 顯示在視圖表面的圖層
CALayer *layer = self.viewContrain.layer;
layer.masksToBounds = true;

_captureVideoPreviewLayer.frame = layer.bounds;
_captureVideoPreviewLayer.masksToBounds = true;
_captureVideoPreviewLayer.videoGravity=AVLayerVideoGravityResizeAspectFill;//填充模式
[layer addSublayer:_captureVideoPreviewLayer];

// 讓會(huì)話(AVCaptureSession)勾搭好輸入輸出心软,然后把視圖渲染到預(yù)覽層上
[_captureSession startRunning];
  1. 將捕獲的音頻或視頻數(shù)據(jù)輸出到指定文件。
//創(chuàng)建一個(gè)拍攝的按鈕著蛙,當(dāng)我們點(diǎn)擊這個(gè)按鈕就會(huì)觸發(fā)視頻錄制删铃,并將這個(gè)錄制的視頻放到 temp 文件夾中
- (IBAction)takeMovie:(id)sender {
[(UIButton *)sender setSelected:![(UIButton *)sender isSelected]];
if ([(UIButton *)sender isSelected]) {
     AVCaptureConnection *captureConnection=[self.caputureMovieFileOutput connectionWithMediaType:AVMediaTypeVideo];
    // 開(kāi)啟視頻防抖模式
    AVCaptureVideoStabilizationMode stabilizationMode = AVCaptureVideoStabilizationModeCinematic;
    if ([self.captureDeviceInput.device.activeFormat isVideoStabilizationModeSupported:stabilizationMode]) {
        [captureConnection setPreferredVideoStabilizationMode:stabilizationMode];
    }

    //如果支持多任務(wù)則則開(kāi)始多任務(wù)
    if ([[UIDevice currentDevice] isMultitaskingSupported]) {
       self.backgroundTaskIdentifier = [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:nil];
    }
    // 預(yù)覽圖層和視頻方向保持一致,這個(gè)屬性設(shè)置很重要,如果不設(shè)置踏堡,那么出來(lái)的視頻圖像可以是倒向左邊的猎唁。
    captureConnection.videoOrientation=[self.captureVideoPreviewLayer connection].videoOrientation;

   // 設(shè)置視頻輸出的文件路徑,這里設(shè)置為 temp 文件
    NSString *outputFielPath=[NSTemporaryDirectory() stringByAppendingString:MOVIEPATH];

    // 路徑轉(zhuǎn)換成 URL 要用這個(gè)方法暂吉,用 NSBundle 方法轉(zhuǎn)換成 URL 的話可能會(huì)出現(xiàn)讀取不到路徑的錯(cuò)誤
    NSURL *fileUrl=[NSURL fileURLWithPath:outputFielPath];

   // 往路徑的 URL 開(kāi)始寫(xiě)入錄像 Buffer ,邊錄邊寫(xiě)
    [self.caputureMovieFileOutput startRecordingToOutputFileURL:fileUrl recordingDelegate:self];
}
else {
   // 取消視頻拍攝
    [self.caputureMovieFileOutput stopRecording];
    [self.captureSession stopRunning];
    [self completeHandle];
}
}

當(dāng)然我們錄制的開(kāi)始與結(jié)束都是有監(jiān)聽(tīng)方法的胖秒,AVCaptureFileOutputRecordingDelegate 這個(gè)代理里面就有我們想要做的

- (void)captureOutput:(AVCaptureFileOutput *)captureOutput didStartRecordingToOutputFileAtURL:(NSURL *)fileURL fromConnections:(NSArray *)connections
{
   NSLog(@"---- 開(kāi)始錄制 ----");
}

- (void)captureOutput:(AVCaptureFileOutput *)captureOutput didFinishRecordingToOutputFileAtURL:(NSURL *)outputFileURL fromConnections:(NSArray *)connections error:(NSError *)error
{
    NSLog(@"---- 錄制結(jié)束 ----");
}

2.2 AVPlayer自定義播放器

AVPlayer是用于管理媒體資產(chǎn)的播放和定時(shí)的控制器對(duì)象,它提供了控制播放器的有運(yùn)輸行為的接口慕的,如它可以在媒體的時(shí)限內(nèi)播放阎肝,暫停,和改變播放的速度肮街,并有定位各個(gè)動(dòng)態(tài)點(diǎn)的能力风题。我們可以使用AVPlayer來(lái)播放本地和遠(yuǎn)程的視頻媒體文件,如QuickTime影片和MP3音頻文件嫉父,以及視聽(tīng)媒體使用HTTP流媒體直播服務(wù)沛硅。

AVPlayer本身并不能顯示視頻,而且它也不像AVPlayerViewController有一個(gè)view屬性绕辖。如果AVPlayer要顯示必須創(chuàng)建一個(gè)播放器層AVPlayerLayer用于展示摇肌,播放器層繼承于CALayer,有了AVPlayerLayer之添加到控制器視圖的layer中即可仪际。要使用AVPlayer首先了解一下幾個(gè)常用的類:

  • AVPlayer:播放器围小,將數(shù)據(jù)解碼處理成為圖像和聲音昵骤。
  • AVAsset:主要用于獲取多媒體信息,是一個(gè)抽象類肯适,不能直接使用变秦。

  • AVURLAsset:AVAsset的子類,可以根據(jù)一個(gè)URL路徑創(chuàng)建一個(gè)包含媒體信息的AVURLAsset對(duì)象框舔。負(fù)責(zé)網(wǎng)絡(luò)連接蹦玫,請(qǐng)求數(shù)據(jù)。

  • AVPlayerItem:一個(gè)媒體資源管理對(duì)象刘绣,管理者視頻的一些基本信息和狀態(tài)樱溉,負(fù)責(zé)數(shù)據(jù)的獲取與分發(fā);一個(gè)AVPlayerItem對(duì)應(yīng)著一個(gè)視頻資源额港〗攘可以添加觀察者監(jiān)聽(tīng)播放源的3個(gè)狀態(tài):

    //添加觀察者
    [_playerItem 
        addObserver:self 
        forKeyPath:@"status"
        options:NSKeyValueObservingOptionNew 
        context:nil];
    //對(duì)播放源的三個(gè)狀態(tài)(status)
    AVPlayerItemStatusReadyToPlay  //播放源準(zhǔn)備好
    AVPlayerItemStatusUnknown    //播放源未知
    AVPlayerItemStatusFailed     //播放源失敗
    
  • AVPlayerLayer:圖像層歧焦,AVPlayer的圖像要通過(guò)AVPlayerLayer呈現(xiàn)移斩。

avplayerLayer_2x.png


使用 AVPlayer 對(duì)象控制資產(chǎn)的播放。在播放期間绢馍,可以使用一個(gè) AVPlayerItem 實(shí)例去管理資產(chǎn)作為一個(gè)整體的顯示狀態(tài)向瓷,AVPlayerItemTrack 對(duì)象來(lái)管理一個(gè)單獨(dú)軌道的顯示狀態(tài)。使用 AVPlayerLayer 顯示視頻舰涌。

需要注意的是猖任,AVPlayer的模式是,你不要主動(dòng)調(diào)用play方法播放視頻瓷耙,而是等待AVPlayerItem告訴你朱躺,我已經(jīng)準(zhǔn)備好播放了,你現(xiàn)在可以播放了摊沉,所以我們要監(jiān)聽(tīng)AVPlayerItem的狀態(tài)锦亦,通過(guò)添加監(jiān)聽(tīng)者的方式獲取AVPlayerItem的狀態(tài)谚攒。當(dāng)監(jiān)聽(tīng)到播放器已經(jīng)準(zhǔn)備好播放的時(shí)候,就可以調(diào)用play方法源请。

創(chuàng)建一個(gè)視頻播放器的思路:

  • 創(chuàng)建一個(gè)view用來(lái)放置AVPlayerLayer
  • 設(shè)置AVPlayer AVPlayerItem 并將 AVPlayer放到AVPlayerLayer中,在將AVPlayerLayer添加到[view.layer addSubLayer]中
  • 添加觀察者彻况,觀察播放源的狀態(tài)
    1. 如果狀態(tài)是AVPlayerItemStatusReadyToPlay就開(kāi)始播放
  • 在做一些功能上的操作
//獲取url 本地url
    //NSURL *url = [[NSBundle mainBundle]URLForResource:@"視頻" withExtension:@"mp4"];
    NSURL *url = [NSURL URLWithString:@"http://XXX.mp4"];
    //創(chuàng)建playerItem
    _playerItem = [AVPlayerItem playerItemWithURL:url];
    //添加item的觀察者 監(jiān)聽(tīng)播放源的播放狀態(tài)(status)
    [_playerItem addObserver:self forKeyPath:@"status" options:NSKeyValueObservingOptionNew context:nil];
    //創(chuàng)建playerItem
    _player = [AVPlayer playerWithPlayerItem:_playerItem];
    //創(chuàng)建playerLayer
    _playerLayer=[AVPlayerLayer playerLayerWithPlayer:_player];
    //設(shè)置_layer的frame
    _playerLayer.frame=CGRectMake(_playerView.frame.origin.x, _playerView.frame.origin.y, _playerView.frame.size.width,300);
    //添加到_playerView中
    [_playerView.layer addSublayer:_playerLayer];
-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSString *,id> *)change context:(void *)context{
  if ([keyPath isEqualToString:@"status"]) {
      AVPlayerItem *playerItem = (AVPlayerItem *)object;
      AVPlayerItemStatus status = playerItem.status;
      switch (status) {
          case AVPlayerItemStatusUnknown:{

          }
              break;
          case AVPlayerItemStatusReadyToPlay:{
              [self.player play];
              self.player.muted = self.mute;
              // 顯示圖像邏輯
              [self handleShowViewSublayers];

          }
              break;
          case AVPlayerItemStatusFailed:{

          }
              break;
          default:
              break;
      }
  }
}

具體代碼可下載Demo

2.3 邊播放谁尸,邊緩存

如果直接使用上面這種方式, URL 通過(guò) AVPlayer 播放纽甘,系統(tǒng)并不會(huì)做緩存處理良蛮,等下次再播又要重新下載,對(duì)流量和網(wǎng)絡(luò)狀態(tài)來(lái)說(shuō)也是蠻尷尬的悍赢。那如何做到邊播放决瞳,邊緩存咬展,下載完成還能直接從本地讀取呢?

答案就是:在本地開(kāi)啟一個(gè) http 服務(wù)器瞒斩,把需要緩存的請(qǐng)求地址指向本地服務(wù)器破婆,并帶上真正的 url 地址。

cachePlan.png

當(dāng)我們給播放器設(shè)置好url等一些參數(shù)后胸囱,播放器就會(huì)向url所在的服務(wù)器發(fā)送請(qǐng)求(請(qǐng)求參數(shù)有兩個(gè)值祷舀,一個(gè)是offset偏移量,另一個(gè)是length長(zhǎng)度烹笔,其實(shí)就相當(dāng)于NSRange一樣)裳扯,服務(wù)器就根據(jù)range參數(shù)給播放器返回?cái)?shù)據(jù)。這就是AVPlayer播放大致的原理谤职。

AVAssetResourceLoaderDelegate

我們?cè)谑褂肁VURLAsset時(shí)饰豺,實(shí)際上是可以設(shè)置AVAssetResourceLoaderDelegate代理。

AVURLAsset *urlAsset = ...
[urlAsset.resourceLoader setDelegate:<AVAssetResourceLoaderDelegate> queue:dispatch_get_main_queue()];

AVAssetResourceLoader是負(fù)責(zé)數(shù)據(jù)加載的允蜈,最最重要的是我們只要遵守了AVAssetResourceLoaderDelegate冤吨,就可以成為它的代理,成為它的代理以后饶套,數(shù)據(jù)加載可能會(huì)通過(guò)代理方法詢問(wèn)我們漩蟆。

只要找一個(gè)對(duì)象實(shí)現(xiàn)了 AVAssetResourceLoaderDelegate 這個(gè)協(xié)議的方法,丟給 asset妓蛮,再把 asset 丟給 AVPlayer怠李,AVPlayer 在執(zhí)行播放的時(shí)候就會(huì)去問(wèn)這個(gè) delegate:喂,你能不能播放這個(gè) url 案蚩恕捺癞?然后會(huì)觸發(fā)下面這個(gè)方法:

- (BOOL)resourceLoader:(AVAssetResourceLoader *)resourceLoader shouldWaitForLoadingOfRequestedResource:(AVAssetResourceLoadingRequest *)loadingRequest 

AVAssetResourceLoadingRequest 里面,request 代表原始的請(qǐng)求构挤,由于 AVPlayer 是會(huì)觸發(fā)分片下載的策略髓介,還需要從dataRequest 中得到請(qǐng)求范圍的信息。有了請(qǐng)求地址和請(qǐng)求范圍儿倒,我們就可以重新創(chuàng)建一個(gè)設(shè)置了請(qǐng)求 Range 頭的 NSURLRequest 對(duì)象版保,讓下載器去下載這個(gè)文件的 Range 范圍內(nèi)的數(shù)據(jù)。

而對(duì)應(yīng)的下載我們可以使用NSURLSession 實(shí)現(xiàn)斷點(diǎn)下載夫否、離線斷點(diǎn)下載等彻犁。

// 替代NSMutableURL, 可以動(dòng)態(tài)修改scheme
NSURLComponents *actualURLComponents = [[NSURLComponents alloc] initWithURL:url resolvingAgainstBaseURL:NO];
actualURLComponents.scheme = @"http";

// 創(chuàng)建請(qǐng)求
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[actualURLComponents URL] cachePolicy:NSURLRequestReloadIgnoringCacheData timeoutInterval:20.0];

// 修改請(qǐng)求數(shù)據(jù)范圍
if (offset > 0 && self.videoLength > 0) {
    [request addValue:[NSString stringWithFormat:@"bytes=%ld-%ld",(unsigned long)offset, (unsigned long)self.videoLength - 1] forHTTPHeaderField:@"Range"];
}

// 重置
[self.session invalidateAndCancel];

// 創(chuàng)建Session,并設(shè)置代理
self.session = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration] delegate:self delegateQueue:[NSOperationQueue mainQueue]];

// 創(chuàng)建會(huì)話對(duì)象
NSURLSessionDataTask *dataTask = [self.session dataTaskWithRequest:request];

// 開(kāi)始下載
[dataTask resume];

我們可以在NSURLSession的代理方法中獲得下載的數(shù)據(jù)凰慈,拿到下載的數(shù)據(jù)以后汞幢,為了解決內(nèi)存暴漲的問(wèn)題,我們使用NSOutputStream微谓,將數(shù)據(jù)直接寫(xiě)入到硬盤(pán)中存放臨時(shí)文件的文件夾森篷。在請(qǐng)求結(jié)束的時(shí)候输钩,我們判斷是否成功下載好文件,如果下載成功仲智,就把這個(gè)文件轉(zhuǎn)移到我們的存儲(chǔ)成功文件的文件夾买乃。如果下載失敗,就把臨時(shí)數(shù)據(jù)刪除钓辆。

// 1.接收到服務(wù)器響應(yīng)的時(shí)候
-(void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveResponse:(NSURLResponse *)response completionHandler:(void (^)(NSURLSessionResponseDisposition))completionHandler;

// 2.接收到服務(wù)器返回?cái)?shù)據(jù)的時(shí)候調(diào)用,會(huì)調(diào)用多次
-(void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data;

// 3.請(qǐng)求結(jié)束的時(shí)候調(diào)用(成功|失敗),如果失敗那么error有值
-(void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error;

代理對(duì)象需要實(shí)現(xiàn)的功能

  • 1.接收視頻播放器的請(qǐng)求剪验,并根據(jù)請(qǐng)求的range向服務(wù)器請(qǐng)求本地沒(méi)有獲得的數(shù)據(jù)

  • 2.緩存向服務(wù)器請(qǐng)求回的數(shù)據(jù)到本地

  • 3.如果向服務(wù)器的請(qǐng)求出現(xiàn)錯(cuò)誤,需要通知給視頻播放器前联,以便視頻播放器對(duì)用戶進(jìn)行提示

視頻播放器處理流程

  • 1.當(dāng)開(kāi)始播放視頻時(shí)功戚,通過(guò)視頻url判斷本地cache中是否已經(jīng)緩存當(dāng)前視頻,如果有似嗤,則直接播放本地cache中視頻
  • 2.如果本地cache中沒(méi)有視頻啸臀,則視頻播放器向代理請(qǐng)求數(shù)據(jù)
  • 3.加載視頻時(shí)展示正在加載的提示(菊花轉(zhuǎn))
  • 4.如果可以正常播放視頻,則去掉加載提示烁落,播放視頻乘粒,如果加載失敗,去掉加載提示并顯示失敗提示
  • 5.在播放過(guò)程中如果由于網(wǎng)絡(luò)過(guò)慢或拖拽原因?qū)е聸](méi)有播放數(shù)據(jù)時(shí)顽馋,要展示加載提示谓厘,跳轉(zhuǎn)到第4步

代理對(duì)象處理流程

  • 1.當(dāng)視頻播放器向代理請(qǐng)求dataRequest時(shí),判斷代理是否已經(jīng)向服務(wù)器發(fā)起了請(qǐng)求寸谜,如果沒(méi)有,則發(fā)起下載整個(gè)視頻文件的請(qǐng)求
  • 2.如果代理已經(jīng)和服務(wù)器建立鏈接属桦,則判斷當(dāng)前的dataRequest請(qǐng)求的offset是否大于當(dāng)前已經(jīng)緩存的文件的offset熊痴,如果大于則取消當(dāng)前與服務(wù)器的請(qǐng)求,并從offset開(kāi)始到文件尾向服務(wù)器發(fā)起請(qǐng)求(此時(shí)應(yīng)該是由于播放器向后拖拽聂宾,并且超過(guò)了已緩存的數(shù)據(jù)時(shí)才會(huì)出現(xiàn))
  • 3.如果當(dāng)前的dataRequest請(qǐng)求的offset小于已經(jīng)緩存的文件的offset果善,同時(shí)大于代理向服務(wù)器請(qǐng)求的range的offset,說(shuō)明有一部分已經(jīng)緩存的數(shù)據(jù)可以傳給播放器系谐,則將這部分?jǐn)?shù)據(jù)返回給播放器(此時(shí)應(yīng)該是由于播放器向前拖拽巾陕,請(qǐng)求的數(shù)據(jù)已經(jīng)緩存過(guò)才會(huì)出現(xiàn))
  • 4.如果當(dāng)前的dataRequest請(qǐng)求的offset小于代理向服務(wù)器請(qǐng)求的range的offset,則取消當(dāng)前與服務(wù)器的請(qǐng)求纪他,并從offset開(kāi)始到文件尾向服務(wù)器發(fā)起請(qǐng)求(此時(shí)應(yīng)該是由于播放器向前拖拽鄙煤,并且超過(guò)了已緩存的數(shù)據(jù)時(shí)才會(huì)出現(xiàn))
  • 5.只要代理重新向服務(wù)器發(fā)起請(qǐng)求,就會(huì)導(dǎo)致緩存的數(shù)據(jù)不連續(xù)茶袒,則加載結(jié)束后不用將緩存的數(shù)據(jù)放入本地cache
  • 6.如果代理和服務(wù)器的鏈接超時(shí)梯刚,重試一次,如果還是錯(cuò)誤則通知播放器網(wǎng)絡(luò)錯(cuò)誤
  • 7.如果服務(wù)器返回其他錯(cuò)誤薪寓,則代理通知播放器網(wǎng)絡(luò)錯(cuò)誤


3. 在線直播和視頻點(diǎn)播亡资,開(kāi)發(fā)流程:

AVPlayer + ffmpeg / ijkplayer

我們平時(shí)看到的視頻文件有許多格式澜共,比如 avi, mkv锥腻, rmvb嗦董, mov, mp4等等瘦黑,這些被稱為容器Container)展懈, 不同的容器格式規(guī)定了其中音視頻數(shù)據(jù)的組織方式(也包括其他數(shù)據(jù),比如字幕等)供璧。容器中一般會(huì)封裝有視頻和音頻軌存崖,也稱為視頻流(stream)和音頻 流,播放視頻文件的第一步就是根據(jù)視頻文件的格式睡毒,解析(demux)出其中封裝的視頻流来惧、音頻流以及字幕(如果有的話),解析的數(shù)據(jù)讀到包 (packet)中演顾,每個(gè)包里保存的是視頻幀(frame)或音頻幀供搀,然后分別對(duì)視頻幀和音頻幀調(diào)用相應(yīng)的解碼器(decoder)進(jìn)行解碼,比如使用 H.264編碼的視頻和MP3編碼的音頻钠至,會(huì)相應(yīng)的調(diào)用H.264解碼器和MP3解碼器葛虐,解碼之后得到的就是原始的圖像(YUV or RGB)和聲音(PCM)數(shù)據(jù),然后根據(jù)同步好的時(shí)間將圖像顯示到屏幕上棉钧,將聲音輸出到聲卡屿脐,最終就是我們看到的視頻。

FFmpeg的API就是根據(jù)這個(gè)過(guò)程設(shè)計(jì)的宪卿,ffmpeg是一個(gè)多平臺(tái)多媒體處理工具的诵,處理視頻和音頻的功能非常強(qiáng)大,能夠解碼佑钾,編碼西疤, 轉(zhuǎn)碼,復(fù)用休溶,解復(fù)用代赁,流,過(guò)濾器和播放大部分的視頻格式兽掰。ffmpeg的代碼是包括兩部分的芭碍,一部分是library,一部分是tool禾进。api都是在library里面豁跑,如果直接調(diào)api來(lái)操作視頻的話,就需要寫(xiě)c或者c++了。另一部分是tool艇拍,使用的是命令行狐蜕,則不需要自己去編碼來(lái)實(shí)現(xiàn)視頻操作的流程。實(shí)際上tool只不過(guò)把命令行轉(zhuǎn)換為api的操作而已卸夕。

ijkplayer

ijkplayer 框架是B站(BiliBili)提供了一個(gè)開(kāi)源的流媒體解決方案层释,集成了 ffmpeg。使用 ijkplayer 框架我們可以很方便地實(shí)現(xiàn)視頻直播功能(HTTP/RTMP/RTSP 這幾種直播源都支持)快集。并且同時(shí)支持 iOS 和 Android 贡羔。


一個(gè)完整直播app實(shí)現(xiàn)流程

1.采集、2.濾鏡處理个初、3.編碼乖寒、4.推流、5.CDN分發(fā)院溺、6.拉流楣嘁、7.解碼、8.播放珍逸、9.聊天互動(dòng)

live_broadcast.png

具體到各個(gè)環(huán)節(jié):推流端(采集逐虚、美顏處理、編碼谆膳、推流)叭爱、服務(wù)端處理(轉(zhuǎn)碼、錄制漱病、截圖买雾、鑒黃)、播放器(拉流缨称、解碼凝果、渲染)、互動(dòng)系統(tǒng)(聊天室睦尽、禮物系統(tǒng)、贊)

一個(gè)完整直播app技術(shù)點(diǎn)

technical_point.jpg

流媒體開(kāi)發(fā):網(wǎng)絡(luò)層(socket或st)負(fù)責(zé)傳輸型雳,協(xié)議層(rtmp或hls)負(fù)責(zé)網(wǎng)絡(luò)打包当凡,封裝層(flv、ts)負(fù)責(zé)編解碼數(shù)據(jù)的封裝纠俭,編碼層(h.264和aac)負(fù)責(zé)圖像沿量,音頻壓縮。

:每幀代表一幅靜止的圖像

GOP
  • 直播的數(shù)據(jù)冤荆,其實(shí)是一組圖片朴则,包括I幀、P幀钓简、B幀乌妒,當(dāng)用戶第一次觀看的時(shí)候汹想,會(huì)尋找I幀,而播放器會(huì)到服務(wù)器尋找到最近的I幀反饋給用戶撤蚊。因此古掏,GOP Cache增加了端到端延遲,因?yàn)樗仨氁玫阶罱腎幀
  • GOP Cache的長(zhǎng)度越長(zhǎng)侦啸,畫(huà)面質(zhì)量越好

碼率:圖片進(jìn)行壓縮后每秒顯示的數(shù)據(jù)量槽唾。

幀率
  • 由于人類眼睛的特殊生理結(jié)構(gòu),如果所看畫(huà)面之幀率高于16的時(shí)候光涂,就會(huì)認(rèn)為是連貫的庞萍,此現(xiàn)象稱之為視覺(jué)暫留。并且當(dāng)幀速達(dá)到一定數(shù)值后忘闻,再增長(zhǎng)的話钝计,人眼也不容易察覺(jué)到有明顯的流暢度提升了。

分辨率:(矩形)圖片的長(zhǎng)度和寬度服赎,即圖片的尺寸

壓縮前的每秒數(shù)據(jù)量:幀率X分辨率(單位應(yīng)該是若干個(gè)字節(jié))

壓縮比:壓縮前的每秒數(shù)據(jù)量/碼率 (對(duì)于同一個(gè)視頻源并采用同一種視頻編碼算法葵蒂,則:壓縮比越高,畫(huà)面質(zhì)量越差重虑。)

視頻文件格式文件的后綴践付,比如.wmv,.mov,.mp4,.mp3,.avi,

  • 主要用處,根據(jù)文件格式缺厉,系統(tǒng)會(huì)自動(dòng)判斷用什么軟件打開(kāi),
    注意: 隨意修改文件格式永高,對(duì)文件的本身不會(huì)造成太大的影響,比如把a(bǔ)vi改成mp4,文件還是avi.

視頻封裝格式一種儲(chǔ)存視頻信息的容器提针,流式封裝可以有TS命爬、FLV等,索引式的封裝有MP4,MOV,AVI等辐脖,

  • 主要作用:一個(gè)視頻文件往往會(huì)包含圖像和音頻饲宛,還有一些配置信息(如圖像和音頻的關(guān)聯(lián),如何解碼它們等):這些內(nèi)容需要按照一定的規(guī)則組織嗜价、封裝起來(lái).
  • 注意:會(huì)發(fā)現(xiàn)封裝格式跟文件格式一樣艇抠,因?yàn)橐话阋曨l文件格式的后綴名即采用相應(yīng)的視頻封裝格式的名稱,所以視頻文件格式就是視頻封裝格式。

視頻封裝格式和視頻壓縮編碼標(biāo)準(zhǔn):就好像項(xiàng)目工程和編程語(yǔ)言久锥,封裝格式就是一個(gè)項(xiàng)目的工程家淤,視頻編碼方式就是編程語(yǔ)言,一個(gè)項(xiàng)目工程可以用不同語(yǔ)言開(kāi)發(fā)瑟由。


另這里有幾款優(yōu)秀的播放器控件可去GitHub下載學(xué)習(xí):

ZFPlayer - 使用人數(shù)也很多絮重。基于AVPlayer,一個(gè)播放器的基本功能幾乎很全面了

mobileplayer-ios - 純 Swift 青伤。文檔詳細(xì)督怜,功能齊全

KRVideoPlayer - 36kr 開(kāi)源項(xiàng)目。類似Weico的播放器

SGPlayer - 支持 VR 播放

VKVideoPlayer - VKVideoPlayer is the same battle tested video player used in our Viki iOS App enjoyed by millions of users all around the world

WMPlayer - AVPlayer的封裝潮模,繼承UIView亮蛔,想怎么玩就怎么玩。支持播放mp4擎厢、m3u8究流、3gp、mov动遭,網(wǎng)絡(luò)和本地視頻同時(shí)支持芬探。全屏和小屏播放同時(shí)支持。自動(dòng)感應(yīng)旋轉(zhuǎn)屏

Player - ?? video player in Swift, simple way to play and stream media on iOS/tvOS


參考文獻(xiàn):

AVAsset

AVFoundation Programming Guide(官方文檔翻譯)完整版

iOS錄制視頻

iOS錄制(或選擇)視頻厘惦,壓縮偷仿、上傳(整理)

iOS視頻錄制壓縮

iOS上傳視頻到服務(wù)器

iOS視頻邊下邊播--緩存播放數(shù)據(jù)流

IOS 微信聊天發(fā)送小視頻的秘密(AVAssetReader+AVAssetReaderTrackOutput播放視頻)

可能是目前最好的 AVPlayer 音視頻緩存方案

[iOS]仿微博視頻邊下邊播之封裝播放器

【補(bǔ)充】NSURLSession 詳解離線斷點(diǎn)下載的實(shí)現(xiàn)

iOS: FFmpeg的使用一

iOS 利用FFmpeg 開(kāi)發(fā)音視頻流(二)——Mac 系統(tǒng)上編譯 iOS 可用的FFmpeg 庫(kù)

iOS中集成ijkplayer視頻直播框架

iOS 直播 —— 推流

【如何快速的開(kāi)發(fā)一個(gè)完整的iOS直播app】(原理篇)

iOS流媒體開(kāi)發(fā)之三:HLS直播(M3U8)回看和下載功能的實(shí)現(xiàn)

即時(shí)通訊音視頻開(kāi)發(fā)(一):視頻編解碼之理論概述

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市宵蕉,隨后出現(xiàn)的幾起案子酝静,更是在濱河造成了極大的恐慌,老刑警劉巖羡玛,帶你破解...
    沈念sama閱讀 216,324評(píng)論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件别智,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡稼稿,警方通過(guò)查閱死者的電腦和手機(jī)薄榛,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,356評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)让歼,“玉大人敞恋,你說(shuō)我怎么就攤上這事∧庇遥” “怎么了硬猫?”我有些...
    開(kāi)封第一講書(shū)人閱讀 162,328評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)改执。 經(jīng)常有香客問(wèn)我浦徊,道長(zhǎng),這世上最難降的妖魔是什么天梧? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,147評(píng)論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮霞丧,結(jié)果婚禮上呢岗,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好后豫,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,160評(píng)論 6 388
  • 文/花漫 我一把揭開(kāi)白布悉尾。 她就那樣靜靜地躺著,像睡著了一般挫酿。 火紅的嫁衣襯著肌膚如雪构眯。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 51,115評(píng)論 1 296
  • 那天早龟,我揣著相機(jī)與錄音惫霸,去河邊找鬼。 笑死葱弟,一個(gè)胖子當(dāng)著我的面吹牛壹店,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播芝加,決...
    沈念sama閱讀 40,025評(píng)論 3 417
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼硅卢,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了藏杖?” 一聲冷哼從身側(cè)響起将塑,我...
    開(kāi)封第一講書(shū)人閱讀 38,867評(píng)論 0 274
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎蝌麸,沒(méi)想到半個(gè)月后点寥,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,307評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡祥楣,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,528評(píng)論 2 332
  • 正文 我和宋清朗相戀三年开财,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片误褪。...
    茶點(diǎn)故事閱讀 39,688評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡责鳍,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出兽间,到底是詐尸還是另有隱情历葛,我是刑警寧澤,帶...
    沈念sama閱讀 35,409評(píng)論 5 343
  • 正文 年R本政府宣布嘀略,位于F島的核電站恤溶,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏帜羊。R本人自食惡果不足惜咒程,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,001評(píng)論 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望讼育。 院中可真熱鬧帐姻,春花似錦稠集、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,657評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至呢铆,卻和暖如春晦鞋,著一層夾襖步出監(jiān)牢的瞬間名惩,已是汗流浹背攒读。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,811評(píng)論 1 268
  • 我被黑心中介騙來(lái)泰國(guó)打工希停, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留贤笆,地道東北人受楼。 一個(gè)月前我還...
    沈念sama閱讀 47,685評(píng)論 2 368
  • 正文 我出身青樓欢顷,卻偏偏與公主長(zhǎng)得像凡简,于是被迫代替她去往敵國(guó)和親贬墩。 傳聞我的和親對(duì)象是個(gè)殘疾皇子因俐,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,573評(píng)論 2 353

推薦閱讀更多精彩內(nèi)容