Bikin Aplikasi iOS Video Tiktok Downloader

Sekitar empat bulan yang lalu, saya membuat aplikasi kecil kecil-an bernama Cincau, aplikasi nya belum ku coba rilis di App Store, jadi ini aplikasi sederhana saja untuk mendownload video TikTok, dengan gaya ku sendiri.

Inspirasi ini, kebetulan saya mendapat referensi API untuk mendownload-nya, gateway nya dari China, menggunakan API dari website Tikwm, ini website memang memberikan fitur download dengan berbagai format watermark atau tidak menggunakan watermark, jadi aku coba ingin integrasikan di iOS.

Koneksi API #

Pertama yang ku lakuin adalah, mencoba API nya terlebih dahulu


let BASE_URL = "https://tikwm.com";

func translateForVideoData(_ url: String) async throws -> TiktokData {

        let url = try makeURL(BASE_URL, path: "/api", videoUrl: url)

        var request = URLRequest(url: url)
        request.httpMethod = "GET"
        request.setValue(USER_AGENT, forHTTPHeaderField: "User-Agent")
        request.setValue("application/json", forHTTPHeaderField: "Content-Type")

        struct Response: Decodable {
            let videoData: TiktokData

            enum CodingKeys: String, CodingKey {
                case videoData = "data"
            }
        }

        let response: Response = try await decodedData(for: request)
        return response.videoData
    }

Dari fungsi di atas, kita sudah memasukkan Response dengan struktur TiktokData, sesuai dengan API yang ku coba untuk mendapatkan source link download.

Setelah itu aku coba menyesuaikan struktur data-nya dari respon JSON-nya, berikut sekilasnya

Respond Tiktok Video Data JSON

https://tikwm.com/api/?url=https://www.tiktok.com/@moviesaftermidnight/video/7314688804038642977&hd=1

dengan parameter url, adalah link video tiktok

Dari sekilas data TiktokData, menjelaskan sedikit, ada tiga URL di sini, salah satu-nya yang play itu video tanpa watermark dengan resolusi biasa dan `hdPlay' video tanpa watermark dengan resolusi tinggi.

Sebenarnya masih banyak variable data lainnya, jika kamu langsung melihat response JSON nya langsung.


struct TiktokData: Decodable {
  let id: String
  let title: String

  let play: URL
  let wmPlay: URL
  let hdPlay: URL

  enum CodingKeys: String, CodingKey {
    case id
    case title

    case play
    case wmPlay = "wmplay"
    case hdPlay = "hdplay"
    case size
}

Selain itu saya juga menambahkan integrasi API untuk menampilkan daftar video dari @username yang di-input. Di halaman ProfileView sendiri.

List Video dari User Respond JSON

Contoh link API-nya menggunakan parameter unique_id untuk mengisi username-nya.

https://www.tikwm.com/api/user/posts?unique_id=@drzaidulakbar.official&hd=1

Ini salah satu bonus fitur yang ku terapkan juga di aplikasi Cincau, jika User mengetik tulisan @username nya di input utama-nya, akan tampil halaman daftar video profile user-nya, jadi tinggal memilih, sekalian langsung bisa didownload di dalamnya.

struct ProfileFeed: Decodable {

    let code: Int
    let msg: String
    let processed_time: Double
    let datas: ListVideos

    struct ListVideos: Decodable {
        let videos: [ProfileVideo]
        let cursor: String
        let hasMore: Bool

        enum CodingKeys: String, CodingKey {
            case videos
            case cursor
            case hasMore
        }
    }

    enum CodingKeys: String, CodingKey {
        case code
        case msg
        case processed_time
        case datas = "data"
    }
}

Membuat Halaman Video Player View #

Sebelum kita bisa memutar video tersebut langsung di aplikasinya

Kita harus menyelipkan AVPlayer di dalam UIViewControllerRepresentable, karena Video Player di SwiftUI mungkin belum support sejak aku mengembangkan aplikasi ini, jadi kita membuat Custom View sendiri di untuk SwiftUI-nya

Contoh AVPlayerView untuk UIViewControllerRepresentable

struct AVPlayerView: UIViewControllerRepresentable {

    let player: AVPlayer

    func makeUIViewController(context: Context) -> AVPlayerViewController {
        AVPlayerViewController()
    }

    func updateUIViewController(_ uiViewController: AVPlayerViewController, context: Context) {
        if uiViewController.player == nil {
            uiViewController.player = player
            uiViewController.showsPlaybackControls = true
//            player.play()
        }
    }

    static func dismantleUIViewController(_ uiViewController: AVPlayerViewController, coordinator: ()) {
        uiViewController.player?.pause()
        uiViewController.player = nil
    }
}

Setelah itu, kita membuat View bernama ShortVideoPlayer untuk halaman sekilas tampilan video-nya, jadi di sini agak tricky selama mengembangkannya, apalagi soal posisi dan menampilkan properti data di atas background video-nya, juga tombol untuk Mute dan menghilangkan tulisan-nya.

struct ShortVideoPlayer: View {

  var body: someView {
      GeometryReader {
        ZStack {
        AVPlayerView(player: model.player).ignoresSafeArea().frame(width: geo.size.height * 16 / 9,
          height: geo.size.height)

          if model.is_loading {
              ProgressView()
                .progressViewStyle(.circular)
                .scaleEffect(CGSize(width: 1.5, height: 1.5))
          }
        }
      }
  }
}

Sekarang bagaimana ShortVideoPlayer tersebut mendapatkan URL link video-nya, pasti ada yang kurangkan dari sini, AVPlayerView juga belum tentu mendapatkan Player-nya, AVPlayerView hanya kosongan televisi-nya, tapi tukang mutarnya belum ada ni, jadi apa yang kita tambah di sini. dari hint-nya yang AVPlayer

Lanjutan ShortVideoPlayer

kita bisa menambah parameter url dan juga StateObject model dari ShortVideoPlayerViewModel. Kayaknya aku pernah membahas basic SwiftUI seperti property wrapper di struktur SwiftUI.


struct ShortVideoPlayer: View {

  let url: URL

  @StateObject var model: ShortVideoPlayerViewModel

  init(url: URL, video_size: Binding<CGSize?>) {
    self.url = url;
    _model = StateObject(wrappedValue, ShortVideoPlayerViewModel(url: url, video_size: video_size)
  }
}

Selanjutnya kita mempersiapkan operasional AVPlayer di ObservableObject, di sini yang mengaktifkan flow ShortVideoPlayer nya, dari video ini loading, hingga kita mute suara-nya.

@MainActor
class ShortVideoPlayerViewModel: ObservableObject {

  private let url: URL
  private let player_item: AVPlayerItem
  let player: AVPlayer

  init(url: URL, video_size: Binding<CGSize?>) {
    self.url = url
    player_item = AVPlayerItem(url: url)
    palyer = AVPlayer(playerItem: player_item)
  }

}

Selanjutnya kita coba tampilkan di tampilan SwiftUI-nya, dengan berisi Input URL yang bisa menerima copy paste dari source lain.

Di dalam DetailVideoView, adalah preview Video-nya sebelum pengguna men-download videonya, di mana pengguna melihat deskripsi post video-nya, pilihan download-nya, bahkan tertera juga judul soundtracknya.


struct DetailVideoView: View {

  @State private var tiktokUrl = ""

  @StateObject private var converseViewModel: ConverseViewModel

  var body: some View {
    ZStack {

      Color(.black).ignoresSafeArea()

      ShortVideoPlayer(url: converseViewModel.playUrl!, video_size: .constant(nil),
      controller: VideoControler(), tapInteraction: {
                withAnimation {
                    show_texts.toggle()
                }
            })
      }.sheet(isPresented: $show_share_sheet) {
        if let videoData = con
      }
  }

Sekarang DetailVideoView-nya sudah beres, sekarang kita kerjakan bagaimana tombol Download itu bekerja,

Ini bagaimana kita mencoba mendownloadnya dengan menggunakan URLSession, dengan disampingkan state agar
progress Download tersebut berlangsung.

func downloadVideo(url: URL) {
     let session = URLSession(configuration: .default, delegate: self, delegateQueue: nil)
     let task = session.dataTask(with: url) { (data, response, error) in
                guard error == nil else {
                    return
                }
                let downloadTask = session.downloadTask(with: url)
                downloadTask.resume()
            }
            task.resume()

        withAnimation {
            self.state = .conversing
            downloading = true
        }

        }

URLSession menggunakan Delegate untuk mengambil progress partikel partikel file yang sedang didownload, jadi kita arahkan untuk mmenyimpan nya langsung di koleksi album video kita

Di dalam fungsi delegate didFinishDownloadingTo

  1. Kita membuat lokasi dokumen video-nya dengan menggunakan FileManager
  2. Sekarang kita membuat data mentah-nya untuk menyimpan ke lokasi dokumen yang sudah disiapkan.
  3. setelah data tersebut, kita bagikan di fungsi saveVideoToAlbum
  4. Diikuti menggunakan State, agar tahu flow prosesnya bagi user.

extension ConverseViewModel:  URLSessionDownloadDelegate {

    func urlSession(_ session: URLSession,
    downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
            guard let data = try? Data(contentsOf: location) else {
                return
            }

            let documentsURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
            let destinationURL = documentsURL.appendingPathComponent("\(title).mp4")
            do {
                try data.write(to: destinationURL)
                saveVideoToAlbum(videoURL: destinationURL, albumName: "Cincau")
                withAnimation {
                    self.state = .conversed(destinationURL)
                    self.downloading = false
                }
            } catch {
                withAnimation {
                    self.state = .error
                    self.downloading = false
                }
                print("Error saving file:", error)
            }
        }

    func urlSession(_ session: URLSession,
    downloadTask: URLSessionDownloadTask,
    didWriteData bytesWritten: Int64,
    totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) {
        totalWritten = downloadTask.progress.fractionCompleted
    }
}

jadi setelah data sudah terbentuk selesai, kita menyimpan nya lewat saveVideoToAlbum, di sini aku kasih hint saja di dalam fungsi tersebut.

Save Video To Album

Menyimpan Video langsung ke Album yang sudah eksis

private func saveVideoToAlbum(videoURL: URL, albumName: String) {
    if albumExists(albumName: albumName) {
      let fetchOptions = PHFetchOptions()
      fetchOptions.predicate = NSPredicate(format: "title = %@", albumName)
      let collection = PHAssetCollection.fetchAssetCollections(with: .album, subtype: .any, options: fetchOp        tions)
      if let album = collection.firstObject {
        saveVideo(videoURL: videoURL, to: album)
      }
    }
}

Pertama kita pastikan album khusus untuk aplikasi yang ter-download sudah terbuat, jika sudah eksis kita bisa menggunakan PHFetchOptions

Jadi kita akan membuat fungsi private saveVideo, dengan menggunakan PHPhotoLibrary


private func saveVideo(videoURL: URL, to album: PHAssetCollection) {
    PHPhotoLibrary.shared().performChanges({
        let assetChangeRequest = PHAssetChangeRequest.creationRequestForAssetFromVideo(atFileURL: videoURL)
        let albumChangeRequest = PHAssetCollectionChangeRequest(for: album)
        let enumeration: NSArray = [assetChangeRequest!.placeholderForCreatedAsset!]
        albumChangeRequest?.addAssets(enumeration)
      }, completionHandler: { success, error in
        if success {
          print("Successfully saved video to album")
        } else {
          print("Error saving video to album: \(error?.localizedDescription ?? "")")
          }
    })
}

Video TikTok sudah tersimpan di album yang sudah di siapkan di aplikasi Photos, secara garis besar begini proses bagaimana kita bisa memanfaatkan API tersebut untuk mendownload video TikTok di iOS.

Referensi:

  1. TikWM, Tiktok Video Download No Watermark High Quality Video https://tikwm.com
  2. Cincau iOS Source Code Github
  3. SwiftUI Docs

🙏🙏🙏

Thanks for reads, hope you enjoyed it, sharing this article on your favorite social media network would be highly appreciated 💖! Sawernya juga boleh