MediaView 0.2.0

MediaView 0.2.0

测试已测试
语言语言 SwiftSwift
许可证 MIT
发布日期最新发布2018年8月
SPM支持 SPM

Andrew Boryk 维护。



MediaView 0.2.0

MediaView

CI Status Version License Platform

屏幕截图

alt tag

描述

MediaView 可以显示图片、视频,现在还可以显示 GIF 和音频!它继承自 UIImageView,并具有从网络上懒加载图片的功能。此外,它还可以显示通过 URL 从磁盘或网络下载的视频。视频包含具有时间线和快进功能的播放器。GIF 可以通过从网络上懒加载或通过 NSData 设置在 MediaView 中显示。下载的 GIF 保存为 UIImage 对象以方便存储。音频也可以通过简单地提供网络或磁盘上的 URL 在播放器中显示。主要新增功能是此 MediaView 有一个队列,并且可以以全屏模式显示 MediaView。有多种功能可以开启或关闭,以自定义视图。

废弃的 Objective-C 版本: ABMediaView

alt tag

目录

示例

要运行示例项目,请首先克隆仓库,然后从示例目录运行pod install

要求

  • 需要iOS 9.0或更高版本
  • 需要自动引用计数(ARC)

功能

  • 显示图像、视频、GIF和音频
  • 图像、视频和GIF的轻松懒加载
  • 可最小化和关闭的全屏幕显示
  • 用于全屏幕播放媒体视图的队列
  • 跟踪缓冲、进度和拖动
  • 自动缓存

未来功能

  • 进度和加载视图
  • 缩放
  • 点击显示详细信息选项(而不是暂停)

如果您有更多功能建议,请通过Twitter向我发消息 @TrepIsLife

安装

MediaView可以通过CocoaPods获得。要安装它,只需将以下行添加到您的Podfile中:

pod "MediaView"

您可以使用以下行将import MediaView添加到您的类中:

import MediaView

用法

调用管理器

可以使用共享的MediaQueue实例来呈现和消失MediaViews。

MediaQueue.shared

通过MediaQueue共享实例,有几个功能可以使用来排队、呈现和消失MediaViews。这些功能中的第一个用于将新的MediaView添加到队列中。如果没有MediaViews在队列中,则新排队的视图将被呈现。此外,可以使用dequeue方法从队列中删除一个MediaView。

// Add mediaView to the queue, and present if queue was previously empty
MediaQueue.shared.queue(mediaView: mediaView)

// Check if mediaView is in queue, and if so, remove it
MediaQueue.shared.dequeue(mediaView: mediaView)

其次,如果想要呈现一个MediaView并跳过队列,可以通过使用'present'函数来完成。调用此函数将消失当前正在呈现的MediaView,将提供的MediaView移到队列的前面,并呈现它。

// Present the mediaView with animation
MediaQueue.shared.present(mediaView: mediaView)

// Present the mediaView with the option to animate
MediaQueue.shared.present(mediaView: mediaView, animated: false)

另一方面,如果想要消失当前显示的MediaView,则可以调用'dismissCurrent'函数。如果视图被最小化,这将使视图移出屏幕。如果不是,则视图将消失。它附带一个完成闭包,以便在消失后执行操作。

MediaQueue.shared.dismissCurrent(animated: true) {
    // Perform action after dismissal
}

以下函数结合了前面两个函数的功能。如果队列中有多个MediaViews,则可以通过在共享Manager上调用'presentNext'函数来显示下一个视图。此函数将消失当前MediaView,并呈现队列中的下一个视图。如果没有其他MediaViews在队列中,则在消失后不会采取任何进一步的操作。

MediaQueue.shared.presentNext()

初始化

MediaView可以通过编程方式初始化,或者通过在界面构建器中从UIImageView派生子类来实现。

// MediaView initiliazed using frame
let mediaView = MediaView(frame: view.frame)

MediaView懒惰地加载其媒体,所需要提供的只是源URL字符串。还有一个完成块,其中返回下载的媒体以供缓存。

// Set the image to be displayed in the mediaView, which will be downloaded and available for caching
mediaView.setImage(url: "http://yoursite.com/yourimage.jpg")

// Similar to the preview method, with a completion handler for when the image has completed downloading
mediaView.setImage(url: "http://yoursite.com/yourimage.jpg") { (image) in
    // Take action after image has been downloaded and set to the mediaView
}

// Set the video to be displayed in the mediaView, which will be downloaded and available for caching
mediaView.setVideo(url: "http://yoursite/yourvideo.mp4")

// Set both the video url, and the thumbnail image for the mediaView, downloading both and making both available for caching
mediaView.setVideo(url: "http://yoursite/yourvideo.mp4", thumbnailUrl: "http://yoursite.com/yourimage.jpg")

// Set the video url for the mediaView, downloading it and making it available for caching, as well as the thumbnail image
mediaView.setVideo(url: "http://yoursite/yourvideo.mp4", thumbnail: UIImage(named: "image.png"))

如果正在加载数据文件夹中的文件,(比如说您从网络下载了一个视频,现在想要显示它),可以从目录中指定内容的URL,通过在MediaView上设置'fileFromDirectory'变量来实现。

// Designates that the file is sourced from the Documents Directory of the user's device
mediaView.isFileFromDirectory = true

MediaView也已支持GIF。要将GIF设置到MediaView中,只需通过URL字符串或Data设置它,它将被下载并设置到视图中。GIF作为UIImages提供,以便于存储。

// GIFs can be displayed in MediaView, where the GIF is sourced from the internet
mediaView.setGIF(url: "http://yoursite/yourgif.gif")

// GIFs can also be displayed via Data
mediaView.setGIF(data: data)

此外,MediaView 也支持音频功能。要将音频设置到 MediaView 中,只需通过 URL 字符串设置即可,它将被下载并设置为查看。

// Set the audio to be displayed in the mediaView
mediaView.setAudio(url: "http://yoursite/youraudio.mp4")

// Set both the audio and thumbnail url for the mediaView
mediaView.setAudio(url: "http://yoursite/youraudio.mp4", thumbnailUrl: "http://yoursite.com/yourimage.jpg")

// Set the audio url for the mediaView, as well as the thumbnail image
mediaView.setAudio(url: "http://yoursite/youraudio.mp4", thumbnail: UIImage(named: "thumbnail.png"))

关于整个应用中的播放功能,已增加新的功能,确保即使在设备处于振动模式下,用户仍然能够播放音频。这些变量设置使得在 MediaView 中播放媒体开始和结束时,音频将相应地启用或禁用,并且可以使用 MediaView 类的这些方法进行设置。

// Toggle this functionality to enable/disable sound to play when an MediaView begins playing, and the user's app is on silent
MediaView.audioTypeWhenPlay = .playWhenSilent

// In addition, toggle this functionality to enable/disable sound to play when an MediaView ends playing, and the user's app is on silent
MediaView.audioTypeWhenStop = .standard

加分功能: GIF 动画也可以用作视频和音频的缩略图。

// Set video for mediaView by URL, and set GIF as thumbnail by URL
mediaView.setVideo(url: "www.video.com/urlHere", thumbnailGIFUrl: "http://yoursite/yourgif.gif")

// Set video for mediaView by URL, and set GIF as thumbnail using Data
mediaView.setVideo(url: "www.video.com/urlHere", thumbnailGIFData: gifData)

// Set audio for mediaView by URL, and set GIF as thumbnail by URL
mediaView.setAudio(url: "www.video.com/urlHere", thumbnailGIFUrl: "http://yoursite/yourgif.gif")

// Set audio for mediaView by URL, and set GIF as thumbnail using Data
mediaView.setAudio(url: "www.video.com/urlHere", thumbnailGIFData: gifData)

新增了一个加分功能,即用户按下并长按 MediaView 时,会显示 GIF 预览。此功能目前适用于视频,并且可以通过以下方法实现。

let thumbnailImage: UIImage = ...
let gifData: Data = ...

// Set video for the MediaView, then the thumbnail UIImage, and the url for the preview GIF
mediaView.setVideo(url: "www.video.com/urlHere", thumbnail: thumbnailImage, previewGIFUrl: "http://yoursite/yourgif.gif")

// Set video for the MediaView, then the thumbnail UIImage, and the Data for the preview GIF
mediaView.setVideo(url: "www.video.com/urlHere", thumbnail: thumbnailImage, previewGIFData: gifData)

// Set video for the MediaView, then the url for the thumbnail image, and the url for the preview GIF
mediaView.setVideo(url: "www.video.com/urlHere", thumbnailUrl: "http://yoursite.com/yourimage.jpg", previewGIFUrl: "http://yoursite/yourgif.gif")

// Set video for the MediaView, then the url for the thumbnail image, and the Data for the preview GIF
mediaView.setVideo(url: "www.video.com/urlHere", thumbnailUrl: "http://yoursite.com/yourimage.jpg", previewGIFData: gifData)

alt tag

非常重要 如果您的应用程序支持设备旋转,您的应用内所有的 MediaView 都需要接收旋转通知。因此,您可能需要实现类似以下内容的功能:这里。以下是我找到的一些最佳实现示例

方法 1:将以下代码块放在应用程序的根视图控制器中,或在初始化 MediaView 的视图控制器中。这将使 MediaView 能够知道用户的设备何时旋转,并相应地旋转。

// If 'viewWillTransitionToSize' is already implemented in your code, add the two MediaViewNotifications to your 'animate:alongsideTransition' block
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
    super.viewWillTransition(to: size, with: coordinator)

    coordinator.animate(alongsideTransition: { _ in
        Notification.post(.mediaViewWillRotateNotification)
    }) { _ in
        Notification.post(.mediaViewDidRotateNotification)
    }
}

方法 2:在 AppDelegate 的 didFinishLaunchingWithOptions 方法中添加一个通知来捕获旋转。我不太喜欢这种实现方式,因为它无法捕获将要旋转的通知,因此会延迟 MediaView 的旋转。

// Notification which should be added to AppDelegate
NotificationCenter.default.addObserver(self, selector: #selector(AppDelegate.rotated), name: NSNotification.Name.UIDeviceOrientationDidChange, object: nil)


// Function called from notification
func rotated() {
    Notification.post(.mediaViewWillRotateNotification)
    Notification.post(.mediaViewDidRotateNotification)
}

关于屏幕旋转,如果您的应用程序的 UI 需要纵向,但您希望 MediaView 在横向上可见,则示例项目中包含处理这种情况的方法。这是一个流行的功能,因此包含在内以简化开发。

最后,当一个 MediaView 完成时并且想要清除它以显示新内容(例如在使用可复用单元格时),有一些方法可以轻松处理此任务。

// Removes image, video, audio and GIF data from the MediaView
- (void) resetMediaInView;

// Calls resetMediaInView and also resets the configurations in the MediaView
- (void) resetVariables;

自定义

MediaView 还提供了一种切换功能,允许用户将 MediaView 滑动以关闭或最小化到右下角。最小化允许用户在不离开 MediaView 的情况下与底层界面交互。如果已经在播放,视频和音频将继续播放,用户可以滑动右边关闭缩小的视图。可以通过设置 MediaView 上的 'swipeMode' 值来设置这些设置。

// User can swipe down to dismiss the MediaView
mediaView.swipeMode = .dismiss

// User can swipe down to minimize the MediaView
mediaView.swipeMode = .minimize

// MediaView should only be dismissed if the 'X' close button is pressed, and not swipable.
mediaView.swipeMode = .none

MediaView 还为视频和音频播放提供了一些选项。其中一项选项是 'allowLooping',该选项用于切换媒体在到达末尾后是否应该重新播放。另一个选项是 'autoPlayAfterPresentation',它用于切换媒体在展示后是否应该自动播放。默认情况下,MediaView 将 'shouldAutoPlayAfterPresentation' 设置为 true。

// Toggle looping functionality
mediaView.allowLooping = true

// Toggle functionality to automatically play videos after presenting
mediaView.shouldAutoPlayAfterPresentation = true

如果您希望全屏 MediaView 在视频播放完毕后关闭,可以在 MediaView 上将值 'shouldDismissAfterFinish' 设置为 true。对于全屏 MediaView,此功能将优先于 'allowLooping'。

mediaView.shouldDismissAfterFinishedPlaying = true

MediaView 在显示视频和音频时显示进度条也有几种启用的选项和编辑选项。

// Enable progress track to show at the bottom of the MediaView
mediaView.shouldShowTrack = true

// Toggles the funtionality which would show remaining time instead of total time on the right label on the track
mediaView.shouldDisplayRemainingTime = true

// Change the font for the labels on the track
let font: UIFont = ...
mediaView.trackFont = font

此外,在全屏显示时,可以将标题和详情设置为显示在MediaView的顶部。

// Set just the title to show
mediaView.setTitle("Fight Club")

// Set a title and details label to show
mediaView.setTitle("What's My Age Again", details: "By: Blink-182")

MediaView具有主题颜色,它将改变轨道以及播放按钮和失败指示器的颜色。

// Changing the theme color changes the color of the play and failed indicators as well as the progress track
mediaView.themeColor = .red

MediaView将根据在视图中设置的contentMode显示图片、视频和GIF。然而,还可以设置contentMode为aspectFill,同时videoGravity设置为aspectFit。

// Setting the contentMode to aspectFit will set the videoGravity to aspectFit as well
mediaView.contentMode = .scaleAspectFit

// If you desire to have the image to fill the view, however you would like the videoGravity to be aspectFit, then you can implement this functionality
mediaView.contentMode = .scaleAspectFill
mediaView.videoAspectFit = true

要实现MediaView全屏功能,需要在相应的MediaView上设置'shouldDisplayFullscreen'值为true。默认情况下,此值为false。

mediaView.shouldDisplayFullscreen = true

如果您想为MediaView使用自定义的播放按钮或失败指示器,应在MediaView上设置'customPlayButton'、'customFailedButton'和'customMusicButton'变量。

// Set a custom image for the play button visible on MediaView's with video or audio
mediaView.customPlayButton = UIImage(named: "play.png")

// Set a custom image for when the mediaView fails to play media
mediaView.customFailButton = UIImage(named: "failed.png")

// Set a custom image for the play button visible for MediaView's specifically with audio, supercedes the customPlayButton
mediaView.customMusicButton = UIImage(named: "playMusic.png")

还有隐藏关闭按钮的功能,这样在全屏弹出的mediaView中就不会显示。此功能仅在'swipeMode'设置为'.minimize'或'dismiss'时允许,否则将没有其他关闭弹出窗口的方法。另外,由于横屏模式下禁用了滑动,关闭按钮仍然在横屏视图中可见。

mediaView.shouldHideCloseButton = true

类似地,还有隐藏可播放媒体(视频/音频)的播放按钮的功能。如果您想将MediaView用作背景视频播放器,这个功能很有用。

mediaView.shouldHidePlayButton = true

在屏幕上存在你不希望隐藏的UIStatusBar,或者需要在屏幕顶部为其他视图保留空间时,MediaView具有将屏幕顶部的子视图偏移以避免隐藏这些视图的能力。设置MediaView的'topOffset'属性将向下移动'closeButton'和其他顶部锚点视图。再次强调,最大的使用案例是设置'topOffset'属性为20px,以避免覆盖UIStatusBar。

mediaView.topBuffer = 20

默认情况下,最小化MediaView和屏幕底部之间有12px的缓冲区。可以通过调整MediaView的'bottomBuffer'值来增加更多空间。这样做可以将mediaView显示在UITabBars和UIToolbars等视图之上,从而避免覆盖需要底边空间保留的视图。

mediaView.bottomBuffer = 0

为了使这些缓冲区更容易使用,我已扩展CGFloat以包括以下值。

// .statusBarBuffer = 20px OR 44px if iPhone X
// .navigationBarBuffer = 44px
// .statusAndNavigationBuffer = 64px OR 104px if iPhone X
// .tabBarBuffer = 49px

mediaView.topBuffer = .statusBarBuffer
mediaView.bottomBuffer = .tabBarBuffer

MediaView具有设置全屏弹出窗口起始框架的功能。此功能与'shouldDisplayFullscreen'结合使用很有用,因为它将允许弹出窗口从具有'shouldDisplayFullscreen'启用的MediaView框架中启动。

// Rect that specifies where the mediaView's frame will originate from when presenting, and will be converted into its position in the mainWindow
mediaView.originRect = view.frame

// Rect that specifies where the mediaView's frame will originate from when presenting, and is already converted into its position in the mainWindow
mediaView.originRectConverted = view.frame

但是,如果你正在使用动态UI,因此无法确定MediaView的originRect,可以将属性'shouldPresentFromOriginRect'设置为true。启用此功能后,全屏MediaView将从展示它的MediaView框架中弹出。如果'shouldPresentFromOriginRect'被启用,那么就没有必要设置'originRect'或'originRectConverted',因为这个属性覆盖了这两个属性。

mediaView.shouldPresentFromOriginRect = true

可以指定MediaView是否将显示在可重用的视图中,这将允许MediaView在不重用的情况下获得更好的UI转换性能。默认情况下假设MediaView将被重用,所以可以设置值为'imageViewNotReused'为true(如果未重用)。

mediaView.imageViewNotReused = true

当MediaView的'isMinimizable'值启用时,可以自定义最小化视图的大小比例。此比例的默认值为预设的ABMediaViewRatioPresetLandscape,它是一个宽屏16:9的纵横比。还有预设的方形(ABMediaViewRatioPresetSquare)和横屏9:16(ABMediaViewRatioPresetPortrait)选项。

// Aspect ratio of the minimized view
mediaView.minimizedAspectRatio = .landscapeRatio
mediaView.minimizedAspectRatio = .square
mediaView.minimizedAspectRatio = .portrait
mediaView.minimizedAspectRatio = .landscapeRatio
mediaView.minimizedAspectRatio = (6.0 / 5.0) // Height/Width

与上述选项配合使用,还可以指定最小化视图拉伸覆盖屏幕宽度的比例。默认情况下,最小化视图拉伸覆盖半个屏幕宽度(0.5比例)。此功能在调整最小化视图大小,且MediaView的'minimizedAspectRatio'大于横向时非常有用。

// Ratio of the screen's width that the minimized view will stretch across
mediaView.minimizedWidthRatio = 0.5

缓存

如果你的项目没有缓存系统,并且需要自动化的缓存系统,MediaView就具有这个功能!使用MediaView,图像和GIF保存在内存中的NSCache中,而视频和音频文件则保存到磁盘中。有几种选项可以用于管理缓存,但让我们先从如何启用自动缓存开始。这可以通过在CacheManager共享实例中设置'cacheMediaWhenDownloaded'的值来完成。此外,通过在MediaView上设置'shouldCacheStreamedMedia',视频将被缓存,因为它们在流式传输时会被缓存。目前,视频是在缓冲区从流中完全加载时进行缓存的。音频仍在开发中。

// Saves media to cache
CacheManager.cacheMediaWhenDownloaded = true

// Caches videos when streaming
mediaView.shouldCacheStreamedMedia = true

如果你想要将视频和音频预加载到缓存中,你可以通过在ABMediaView的共享Manager上指定'shouldPreloadPlayableMedia',使MediaView始终在下设音频或视频URL到MediaView时下载视频和音频。但是,如果你只想在单个实例的基础上预加载视频或音频,可以使用'preloadVideo'和'preloadAudio'来完成。如果你不希望预加载视频或音频,只需将'shouldCacheStreamedMedia'设置为true即可,那么视频和音频将进行流式传输。

// Ensure that all video and audio is preloaded before playing, instead of just streaming (works best if your app plays videos/audio that is short in length)
mediaView.shouldPreloadPlayableMedia = true

// Preload the video for this specific mediaView
mediaView.preloadVideo()

// Preload the audio for this specific mediaView
mediaView.preloadAudio()

如果想要清除图像和GIF的内存缓存,只需在MediaView共享Manager上将'shouldCacheMedia'设置为false即可。但是,为了清除用于Documents目录和tmp目录的磁盘缓存,CacheManager提供了一个简单的类函数来清除这些缓存。

// Clear all of the documents directory of cached items in the ABMedia folder
CacheManager.clear(directory: .all)

// Clear the video directory of cached items in the ABMedia folder
CacheManager.clear(directory: .video)

// Clear the audio directory of cached items in the ABMedia folder
CacheManager.clear(directory: .audio)

// Clear all of the temp directory of cached items
CacheManager.clear(directory: .temp)

委托

有一个委托,其中有可选的方法来确定MediaView何时播放或暂停了AVPlayer中的视频,以及视图最小化的程度。

/// A listener to know what percentage that the view has minimized, at a value from 0 to 1
func mediaView(_ mediaView: MediaView, didChangeOffset offsetPercentage: CGFloat)

/// When the mediaView begins playing a video
func didPlayMedia(for mediaView: MediaView)

/// When the mediaView fails to play a video
func didFailToPlayMedia(for mediaView: MediaView)

/// When the mediaView pauses a video
func didPauseMedia(for mediaView: MediaView)

此外,还有一些委托方法可以帮助判断MediaView即将显示、已被显示、即将消失和已消失的情况。

/// Called when the mediaView has begun the presentation process
func willPresent(mediaView: MediaView)

/// Called when the mediaView has been presented
func didPresent(mediaView: MediaView)

/// Called when the mediaView has begun the dismissal process
func willDismiss(mediaView: MediaView)

/// Called when the mediaView has completed the dismissal process. Useful if not looking to utilize the dismissal completion block
func didDismiss(mediaView: MediaView)

如果想要确定媒体视图是否已播放完其视频,可以利用'didFinishPlayableMedia:withLoop:'方法。这还指定了媒体视图在播放完成后是否设置为循环。

/// When the mediaView finishes playing a video, and whether it looped
func didFinishPlayableMedia(for mediaView: MediaView, withLoop didLoop: Bool)

以下委托方法在判断媒体视图是否已经开始、进行中或已完成最小化时很有用。此用法的一个流行场景是根据MediaView是否可见来调整UIStatusBarStyle。

/// Called when the mediaView is in the process of minimizing, and is about to make a change in frame
func willChangeMinimization(for mediaView: MediaView)

/// Called when the mediaView is in the process of minimizing, and has made a change in frame
func didChangeMinimization(for mediaView: MediaView)

/// Called before the mediaView ends minimizing, and informs whether the minimized view will snap to minimized or fullscreen mode
func willEndMinimizing(for mediaView: MediaView, atMinimizedState isMinimized: Bool)

/// Called when the mediaView ends minimizing, and informs whether the minimized view has snapped to minimized or fullscreen mode
func didEndMinimizing(for mediaView: MediaView, atMinimizedState isMinimized: Bool)

另一方面,如果一个MediaView上的'swipeMode'值设置为'.dismiss',会提供委托方法来监听MediaView开始和结束消失过程的时间。

/// Called when the mediaView is in the process of minimizing, and is about to make a change in frame
func willChangeDismissing(for mediaView: MediaView)

/// Called when the mediaView is in the process of minimizing, and has made a change in frame
func didChangeDismissing(for mediaView: MediaView)

/// Called before the mediaView ends minimizing, and informs whether the minimized view will snap to minimized or fullscreen mode
func willEndDismissing(for mediaView: MediaView, withDismissal didDismiss: Bool)

/// Called when the mediaView ends minimizing, and informs whether the minimized view has snapped to minimized or fullscreen mode
func didEndDismissing(for mediaView: MediaView, withDismissal didDismiss: Bool)

如果想要检测MediaView中包含的图像是否已设置或更改,可以监听以下委托方法。

/// Called when the 'image' value of the UIImageView has been set
func mediaView(_ mediaView: MediaView, didSetImage image: UIImage)

如果您想要缓存通过 MediaView 下载的图片、视频或 GIF,已经通过代理来处理这些对象。

/// Called when the mediaView has completed downloading the image from the web
func mediaView(_ mediaView: MediaView, didDownloadImage image: UIImage)

/// Called when the mediaView has completed downloading the video from the web
func mediaView(_ mediaView: MediaView, didDownloadVideo video: URL)

/// Called when the mediaView has completed downloading the audio from the web
func mediaView(_ mediaView: MediaView, didDownloadAudio audio: URL)

/// Called when the mediaView has completed downloading the gif from the web
func mediaView(_ mediaView: MediaView, didDownloadGif gif: UIImage)

最后,如果您在 MediaView 上设置了标题或详情值,您可以在以下代理方法中接收这些标签的触摸。

/// Called when the user taps the title label
func handleTitleSelection(in mediaView: MediaView)

/// Called when the user taps the details label
func handleDetailsSelection(in mediaView: MediaView)

补充库

  • ABVolumeControl:通过不同的样式覆盖 MPVolumeView,并提供一个代理以实现自定义体积视图。
  • ABUtils:一组有用的方法,可以集成到任何项目中。
  • ABKeyboardAccessory:UIView 的子类,可以用作“输入辅助”,并提供代理方法来了解键盘框架何时改变,例如出现和消失。

作者

Andrew Boryk,[email protected]

在 Twitter 上联系我:@TrepIsLife alt text

许可证

MediaView 使用 MIT 许可证提供。有关更多信息,请参阅 LICENSE 文件。