[iOS] Vimeoの動画をネイティブUIに埋め込みたい
こんにちは。きんくまです。
今回はVimeoの動画をネイティブUIに埋め込みたいです。
Vimeoって?
動画共有サービスです。個人的なイメージは、アートとかファッション、映画のメイキングとかおしゃれ系動画サイトという感じですね。
できたもの
Github
cm-tsmaeda / EmbedVimeoIntoNativeView
ネイティブUIの中にVimeoの動画を埋め込みたかったです。
プログラムの流れ
iOSのSDKが欲しかったです。調べてみたところ、存在はするのですがdeprecated扱いで、今後はなくなるとのこと。
なので、ネイティブでのUI埋め込みはあきらめて、部分的にWebViewを使って埋め込むことにしました。
プログラムの流れ アプリ側 1. 動画情報をAPIから取得 2. 1から幅と高さが得られるので、WebViewの高さを設定 3. WebViewにクエリをつけてURLを読み込む Web側 1. URLのクエリから動画idを取得 2. 動画情報をAPIから取得 3. 2から埋め込み用のhtmlが得られるので、それを使ってVimeo用のPlayerを埋め込み
今回は、アプリ側とWeb側を用意しました。
Web側は、htmlを作成して、Webサーバーにホスティングします。
実はWeb側を用意しなくても、htmlの文字列さえあればWebViewで同じことをすることは可能です。
では、どうしてWebサーバーにホスティングするかというと、セキュリティで制限かけられている動画も読み込ませたいためです。
制限のかかったドメインに、読み込み用のhtmlをホスティングすることでセキュリティの制限を回避します。
oEmbed API
Vimeoでは埋め込み動画情報を取得するAPIが用意されています。
Working with oEmbed: Embedding Videos
リクエスト例
https://vimeo.com/api/oembed.json?responsive=true&url=https%3A%2F%2Fvimeo.com%2F601252574
レスポンス例
{ "type": "video", "version": "1.0", "provider_name": "Vimeo", "provider_url": "https://vimeo.com/", "title": "Vogue - History of American Fashion", "author_name": "Andrew B. Myers", "author_url": "https://vimeo.com/andrewbmyers", "is_plus": "1", "account_type": "plus", "html": "<div style=\"padding:56.25% 0 0 0;position:relative;\"><iframe src=\"https://player.vimeo.com/video/601252574?h=bd62b143a3&app_id=122963\" frameborder=\"0\" allow=\"autoplay; fullscreen; picture-in-picture\" allowfullscreen style=\"position:absolute;top:0;left:0;width:100%;height:100%;\" title=\"Vogue - History of American Fashion\"></iframe></div><script src=\"https://player.vimeo.com/api/player.js\"></script>", "width": 426, "height": 240, "duration": 530, "description": "A look at American fashion in the 20th century, with its evolution from simple, European inspired design to the creative work of new and BIPOC designers in the 21st century.", "thumbnail_url": "https://i.vimeocdn.com/video/1237215280_295x166", "thumbnail_width": 295, "thumbnail_height": 166, "thumbnail_url_with_play_button": "https://i.vimeocdn.com/filter/overlay?src0=https%3A%2F%2Fi.vimeocdn.com%2Fvideo%2F1237215280_295x166&src1=http%3A%2F%2Ff.vimeocdn.com%2Fp%2Fimages%2Fcrawler_play.png", "upload_date": "2021-09-09 15:00:15", "video_id": 601252574, "uri": "/videos/601252574" }
これで動画の幅と高さがわかります。
またhtmlというプロパティは、埋め込み用のhtml文字列が入っていますので、これをそのままhtmlに追加すればよさそうです。
アプリ側
Storyboadはこんな感じにUILabel, UIImageView, WKWebViewをUIScrollViewに並べました。
VimeoAPI.swift
struct VimeoGetVideoInfoRequest { let baseUrlStr: String = "https://vimeo.com/api/oembed.json?responsive=true&url=" let videoId: String var url: URL? { let videoUrl = "https://vimeo.com/\(videoId)" guard let encoded = videoUrl.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed) else { return nil } return URL(string: baseUrlStr + encoded) } } /// 動画情報 struct VimeoVideoInfo: Codable { let videoId: Int let width: Int let height: Int enum CodingKeys: String, CodingKey { case videoId = "video_id" case width case height } } enum VimeoError: Error { case fetch case other(Error) static func map(_ error: Error) -> VimeoError { if let vimeoError = error as? VimeoError { return vimeoError } return .other(error) } }
ViewController.swift
import UIKit import WebKit import Combine class ViewController: UIViewController { var subscriptions: Set<AnyCancellable> = [] @IBOutlet weak var contentView: UIView! @IBOutlet weak var contentViewHeightConstraint: NSLayoutConstraint! @IBOutlet weak var largeImageView: UIImageView! @IBOutlet weak var webViewHeightConstraint: NSLayoutConstraint! @IBOutlet weak var webView: WKWebView! var videoInfo: VimeoVideoInfo? let videoId: String = "601252574" override func viewDidLoad() { super.viewDidLoad() contentViewHeightConstraint.constant = 1200 webView.isHidden = true fetchVimeoVideoInfo(videoId: videoId) } func setupWebView() { guard let videoInfo = videoInfo else { return } let screenW = UIScreen.main.bounds.width let webViewHeight: CGFloat = (screenW - 30 * 2) * CGFloat(videoInfo.height) / CGFloat(videoInfo.width) webViewHeightConstraint.constant = webViewHeight guard let url = URL(string: "http://localhost:8090/?id=\(videoInfo.videoId)") else { return } let req = URLRequest(url: url) webView.navigationDelegate = self webView.load(req) } func fetchVimeoVideoInfo(videoId: String) { createVimeoGetVideoInfoRequest(videoId: videoId) .receive(on: DispatchQueue.global(qos: .userInitiated)) .sink(receiveCompletion: { [weak self] completion in guard let self = self else { return } switch completion { case .finished: DispatchQueue.main.async { self.setupWebView() } case .failure(let error): print("Error: \(error.localizedDescription)") } }, receiveValue: { [weak self] videoInfo in self?.videoInfo = videoInfo }).store(in: &subscriptions) } func createVimeoGetVideoInfoRequest(videoId: String) -> AnyPublisher<VimeoVideoInfo, VimeoError> { let api = VimeoGetVideoInfoRequest(videoId: videoId) guard let url = api.url else { fatalError("cannot make url") } let req = URLRequest(url: url) return URLSession.shared.dataTaskPublisher(for: req) .tryMap { (data, response) -> Data in guard let httpRes = response as? HTTPURLResponse, 200 ..< 300 ~= httpRes.statusCode else { throw VimeoError.fetch } return data } .decode(type: VimeoVideoInfo.self, decoder: JSONDecoder()) .mapError{ VimeoError.map($0) } .eraseToAnyPublisher() } } extension ViewController: WKNavigationDelegate { func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { if webView.isHidden { webView.isHidden = false } } }
WebViewでページを読み込むときに、動画のidをクエリにつけています。これをWeb側で読み取ります。
http://localhost:8090/?id={video_id}
Web側
index.html
<!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Vimeoサンプル</title> <link rel="stylesheet" href="vimeo.css"> </head> <body> <script src="vimeo.js"></script> </body> </html>
vimeo.js
function getParameterByName(name) { const match = RegExp('[?&]' + name + '=([^&]*)').exec(window.location.search); return match && decodeURIComponent(match[1].replace(/\+/g, ' ')); } function fetchVideoInfo(id) { const videoUrl = "https://vimeo.com/" + id; const encoded = encodeURIComponent(videoUrl); const fetchUrl = "https://vimeo.com/api/oembed.json?responsive=true&url=" + encoded; fetch(fetchUrl) .then(function(response) { if (!response.ok) { throw new Error("HTTP Error!"); } return response.json(); }) .then(function(response) { //console.log(response); embedVideo(response.html); }); } function embedVideo(htmlStr) { const body = document.getElementsByTagName('body')[0]; body.insertAdjacentHTML('afterbegin', htmlStr); //console.log('embed', htmlStr); } const videoId = getParameterByName('id'); fetchVideoInfo(videoId);
余談。感想
一番わからなかったところは、どうやって動画情報を取得するかでした。
今回使ったoEmbedの方ではない、APIも用意されていて、はじめはそれを調べていました。
そちらの方はアプリの登録が必要だったり、回数制限があったりして困ったのですが、oEmbedの方が見つかりました。oEmbedの方は特に制限もなかったのでよかったです。
ではでは。