技術ブログ

プログラミング、IT関連の記事中心

動画や画像の圧縮【Swift】

■はじめに

画像(写真)や、動画(ビデオ)などを扱っていると、容量が大きくてサーバーへのアップロードに時間がかかったり
端末の容量の圧迫につながったりと言う事があります。

このような場合に、容量を抑える方法を記載します。

◾️画像の圧縮(縮小)

画像をサーバーへ送信したりするアプリだと、画像が大きすぎて送信に時間がかかるという問題があります。
※カメラの性能が良くなった事が原因ですね。。。

この様な場合には、必要なサイズに画像を縮小して少しでも画像サイズを減らした上で送信する事をお勧めします。
実際にどの様に処理をするかを以下に記載します。

「targetImage」という「UIImage」型の変数に縮小前の画像が入っている前提です。

// 取得した画像の縦サイズ、横サイズを取得する
int originalWidth = targetImage.size.width;
int originalHeight = targetImage.size.height;

// リサイズする倍率を作成する。
float scale = 0.0f;
float maxSize = 100.0f;
if (originalWidth > originalHeight) {
    scale = maxSize / originalHeight;
} else {
    scale = maxSize / originalWidth;
}

// 縦横比を合わせた状態でリサイズするためのサイズ設定
CGSize resizedSize = CGSizeMake(originalWidth * scale, originalHeight * scale);
// コンテキストへコピー
UIGraphicsBeginImageContext(resizedSize);
// 元のイメージを用意したサイズに変換
[targetImaged drawInRect:CGRectMake(0, 0, resizedSize.width, resizedSize.height)];
// 変換してイメージを「completionImage」に書き出す
UIImage* completionImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();

◾️動画の圧縮(縮小)

動画の場合には、画像と違い1つの動画で結構容量が大きいですよね。
ここでは、動画の容量を縮小させるために「動画を生成し直す」方法を説明します。

単に動画を生成し直すだけでは、容量は抑える事ができません。
動画を生成し直す時に、「動画のサイズ」「フレームレート」などを下げて生成し直す事で容量を抑える事ができます。

以下のメソッドを使用する事で、生成し直した動画をDocumentディレクトリ内に生成できます。
このメソッドを呼び出す時には、引数に生成し直す元の動画のURLを渡してください。

func compression(beforeMovieUrl: URL) {
    let videoURL: URL = getDocumentsDirectory().appendingPathComponent("\(Date().millisecondsSince1970)tmpMovieSample.mov")
    //コンポジション作成
    let mixComposition : AVMutableComposition = AVMutableComposition()
    //アセットの作成
    //動画のアセットとトラックを作成
    let videoAsset: AVURLAsset = AVURLAsset(url: beforeMovieUrl, options:nil)
    // 元の動画ファイルから、動画(音声なし)トラックの取得
    let videoTrack: AVAssetTrack = (videoAsset.tracks(withMediaType: AVMediaType.video))[0]
    // ベースとなる動画のコンポジション作成
    let compositionVideoTrack: AVMutableCompositionTrack! = mixComposition.addMutableTrack(withMediaType: AVMediaType.video, preferredTrackID: kCMPersistentTrackID_Invalid)
    // 動画の長さ設定
    try! compositionVideoTrack.insertTimeRange(CMTimeRangeMake(start: CMTime.zero, duration: videoAsset.duration), of: videoTrack, at: CMTime.zero)
    // 音声付きの動画か判定
    if videoAsset.tracks(withMediaType: AVMediaType.audio).count > 0 {
        // 音声付き動画の場合、音声を取得し設定する
        // 元の動画ファイルから、オーディオトラックの取得
        let audioTrack: AVAssetTrack = (videoAsset.tracks(withMediaType: AVMediaType.audio))[0]
        // ベースとなる音声のコンポジション作成
        let compositionAudioTrack: AVMutableCompositionTrack! = mixComposition.addMutableTrack(withMediaType: AVMediaType.audio, preferredTrackID: kCMPersistentTrackID_Invalid)
        // 音声の長さ設定
        try! compositionAudioTrack.insertTimeRange(CMTimeRangeMake(start: CMTime.zero, duration: videoAsset.duration), of: audioTrack, at: CMTime.zero)
    }
    // 回転方向の設定
    compositionVideoTrack.preferredTransform = videoTrack.preferredTransform
    // 動画のサイズを取得
    var videoSize: CGSize = videoTrack.naturalSize
    // 動画が縦の場合は「true」で、横の場合には「false」が入る
    var isPortrait: Bool = false
    // ビデオを縦横方向
    let transform: CGAffineTransform = videoTrack.preferredTransform
    // 動画が縦かどうかを判定する
    if (transform.a == 0 && transform.d == 0 && (transform.b == 1.0 || transform.b == -1.0) && (transform.c == 1.0 || transform.c == -1.0)) {
        // 動画が縦の場合
        isPortrait = true
        // 「videoSize」に設定している「高さ」と「幅」を反転させる(デフォルトが横向きのサイズの為)
        videoSize = CGSize(width: videoSize.height, height: videoSize.width)
    }
    // 合成用コンポジション作成
    let videoComp: AVMutableVideoComposition = AVMutableVideoComposition()
    videoComp.renderSize = videoSize
    // フレームレートの設定
    // TODO: ここの値を調整する事で、動画のフレームレートを落として、容量の削減につながる
    videoComp.frameDuration = CMTimeMake(value: 1, timescale: 30)
    // インストラクションを合成用コンポジションに設定
    let instruction: AVMutableVideoCompositionInstruction = AVMutableVideoCompositionInstruction()
    instruction.timeRange = CMTimeRangeMake(start: CMTime.zero, duration: videoAsset.duration)
    let layerInstruction: AVMutableVideoCompositionLayerInstruction = AVMutableVideoCompositionLayerInstruction.init(assetTrack: compositionVideoTrack)
    instruction.layerInstructions = [layerInstruction]
    videoComp.instructions = [instruction]
    if isPortrait {
        // 動画が縦の場合
        // 動画を90%回す(「AVAssetExportSession」を使用する場合には、縦の場合でも横向きで表示される為、90%回す必要がある)
        let FirstAssetScaleFactor: CGAffineTransform = CGAffineTransform(scaleX: 1.0, y: 1.0)
        layerInstruction.setTransform(videoTrack.preferredTransform.concatenating(FirstAssetScaleFactor), at: CMTime.zero)
    }
    // 動画のコンポジションをベースにAVAssetExportを生成
    _assetExport = AVAssetExportSession.init(asset: mixComposition, presetName: AVAssetExportPresetHighestQuality)
    // 合成用コンポジションを設定
    _assetExport?.videoComposition = videoComp
    let exportUrl: URL = videoURL
    _assetExport?.outputFileType = AVFileType.mov
    _assetExport?.outputURL = exportUrl
    _assetExport?.shouldOptimizeForNetworkUse = true
    // エクスポート実行
    _assetExport?.exportAsynchronously(completionHandler: {() -> Void in
        if self._assetExport?.status == AVAssetExportSession.Status.failed {
            // 失敗した場合
        }
        if self._assetExport?.status == AVAssetExportSession.Status.completed {
            // 成功した場合
        }
    })
}

ちなみに、フレームレートや、サイズなどを変更せずに単に新しい動画にコピーしたい場合には、以下のメソッドを呼び出す事でコピーできます。
※引数にはコピーする動画のURLを渡してください。

func compression(beforeMovieUrl: URL) {
    let videoURL: URL = getDocumentsDirectory().appendingPathComponent("\(Date().millisecondsSince1970)tmpMovie.mov")
    //アセットの作成
    //動画のアセットとトラックを作成
    let videoAsset: AVURLAsset = AVURLAsset(url: beforeMovieUrl, options:nil)
    // 動画のコンポジションをベースにAVAssetExportを生成
    self._assetExport = AVAssetExportSession.init(asset: videoAsset, presetName: AVAssetExportPresetHighestQuality)
    let exportUrl: URL = videoURL
    self._assetExport?.outputFileType = AVFileType.mov
    self._assetExport?.outputURL = exportUrl
    // エクスポート実行
    self._assetExport?.exportAsynchronously(completionHandler: {() -> Void in
        if self._assetExport?.status == AVAssetExportSession.Status.failed {
            // 失敗した場合
        }
        if self._assetExport?.status == AVAssetExportSession.Status.completed {
            // 成功した場合
        }
    })
}