当前位置: 首页 > news >正文

望野的翻译网络优化app哪个好

望野的翻译,网络优化app哪个好,做视频网站被判刑,spring mvc 做网站前言 在我之前的文章 《以不同的形式在安卓中创建GIF动图》 中,我挖了一个坑,可以通过录制屏幕后转为 GIF 的方式来创建 GIF。只是当时我只是提了这么一个思路,并没有给出录屏的方式,所以本文的内容就是教大家如何通过调用系统 A…

前言

在我之前的文章 《以不同的形式在安卓中创建GIF动图》 中,我挖了一个坑,可以通过录制屏幕后转为 GIF 的方式来创建 GIF。只是当时我只是提了这么一个思路,并没有给出录屏的方式,所以本文的内容就是教大家如何通过调用系统 API 的方式录制屏幕。

开始实现

技术原理

在安卓 5.0 之前,我们是无法通过常规的方式来录制屏幕或者截图的,要么只能 ROOT,要么就是只能用一些很 Hack 的方式来实现。

不过在安卓 5.0 后,安卓开放了 MediaProjectionManagerVirtualDisplay 等 API,使得普通应用录屏成为了可能。

简单来说,录屏的流程如下:

  1. 拿到 MediaProjectionManager 对象
  2. 通过 MediaProjectionManager.createScreenCaptureIntent() 拿到请求权限的 Intent ,然后用这个 Intent 去请求权限并拿到一个权限许可令牌(resultData,本质上还是个 Intent)。
  3. 通过拿到的 resultData 创建 VirtualDisplay投影。
  4. VirtualDisplay 将图像数据渲染至 Surface 中,最终,我们可以将 Surface 的数据流写入并编码至视频文件。(Surface 可以由 MediaCodec 创建,而 MediaMuxer 可以将 MediaCodec 的数据编码至视频文件中)

从上面的流程可以看出,其实核心思想就是通过 VirtualDisplay 拿到当前屏幕的数据,然后绕一圈将这个数据写入视频文件中。

VirtualDisplay 顾名思义,其实是用来做虚拟屏幕或者说投影的,但是这里并不妨碍我们通过它来录屏啊。

不过由于我们是通过虚拟屏幕来实现录屏的,所以如果应用声明了禁止投屏或使用虚拟屏幕,那么我们录制的内容将是空白的(黑屏)。

准备工作

明白了实现原理之后,我们需要来做点准备工作。

首先是做好界面布局,在主入口编写布局:

override fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)setContent {val context = LocalContext.currentScreenRecordTheme {// A surface container using the 'background' color from the themeSurface(modifier = Modifier.fillMaxSize(),color = MaterialTheme.colors.background) {Column(modifier = Modifier.fillMaxSize(),verticalArrangement = Arrangement.Center,horizontalAlignment = Alignment.CenterHorizontally) {Button(onClick = {startServer(context)}) {Text(text = "启动")}}}}}
}

布局很简单,就是居中显示一个启动按钮,点击按钮后启动录屏服务(Server),这里因为我们的需求是需要录制所有应用界面,而非本APP的界面,所以需要使用一个前台服务并显示一个悬浮按钮用于控制录屏开始与结束。

所以我们需要添加悬浮窗权限,并动态申请:

添加权限: <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />

检查并申请权限:

if (Settings.canDrawOverlays(context)) {// ……// 已有权限
}
else {// 跳转到系统设置手动授予权限(这里其实可以直接跳转到当前 APP 的设置页面,但是不同的定制 ROM 设置页面路径不一样,需要适配,所以我们直接跳转到系统通用设置让用户自己找去)Toast.makeText(context, "请授予“显示在其他应用上层”权限后重试", Toast.LENGTH_LONG).show()val intent = Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION,Uri.parse("package:${context.packageName}"))context.startActivity(intent)
}

悬浮界面权限拿到后就是申请投屏权限。

首先,定义 Activity Result Api,并在获取到权限后将 ResultData 传入 Server,最后启动 Server:

private lateinit var requestMediaProjectionLauncher: ActivityResultLauncher<Intent>override fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)// ……requestMediaProjectionLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {if (it.resultCode == Activity.RESULT_OK && it.data != null) {OverlayService.setData(it.data!!)startService(Intent(this, OverlayService::class.java))}else {Toast.makeText(this, "未授予权限", Toast.LENGTH_SHORT).show()}}
}

然后,在按钮的点击回调中启动这个 Launcher:

val mediaProjectionManager = getSystemService(MEDIA_PROJECTION_SERVICE) as MediaProjectionManager
requestMediaProjectionLauncher.launch(mediaProjectionManager.createScreenCaptureIntent()
)

在这里我们通过 getSystemService 方法拿到了 MediaProjectionManager ,并通过 mediaProjectionManager.createScreenCaptureIntent() 拿到请求权限的 Intent。

最终在授予权限后启动录屏 Server。

但是,这里有一点需要特别注意,由于安卓系统限制,我们必须使用前台 Server 才能投屏,并且还需要为这个前台 Server 显式设置一个通知用于指示 Server 正在运行中,否则将会抛出异常。

所以,添加前台服务权限:

<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />

然后在我们的录屏服务中声明前台服务类型:

<serviceandroid:name=".overlay.OverlayService"android:enabled="true"android:exported="false"android:foregroundServiceType="mediaProjection" />

最后,我们需要为这个服务绑定并显示一个通知:

private fun initRunningTipNotification() {val builder = Notification.Builder(this, "running")builder.setContentText("录屏运行中").setSmallIcon(R.drawable.ic_launcher_foreground)val notificationManager = getSystemService(NOTIFICATION_SERVICE) as NotificationManagerval channel = NotificationChannel("running","显示录屏状态",NotificationManager.IMPORTANCE_DEFAULT)notificationManager.createNotificationChannel(channel)builder.setChannelId("running")startForeground(100, builder.build())
}

需要注意的是,这里我们为了方便讲解,直接将创建和显示通知都放到了点击悬浮按钮后,并且停止录屏后也没有销毁通知。

各位在使用的时候需要根据自己需求改一下。

自此,准备工作完成。

哦,对了,关于如何使用 Compose 显示悬浮界面,因为不是本文重点,而且我也是直接套大佬的模板,所以这里就不做讲解了,感兴趣的可以自己看源码。

下面开始讲解如何录屏。

开始录屏

首先,我们编写了一个简单的帮助类 ScreenRecorder

class ScreenRecorder(private var width: Int,private var height: Int,private val frameRate: Int,private val dpi: Int,private val mediaProjection: MediaProjection?,private val savePath: String
) {private var encoder: MediaCodec? = nullprivate var surface: Surface? = nullprivate var muxer: MediaMuxer? = nullprivate var muxerStarted = falseprivate var videoTrackIndex = -1private val bufferInfo = MediaCodec.BufferInfo()private var virtualDisplay: VirtualDisplay? = nullprivate var isStop = false/*** 停止录制* */fun stop() {isStop = true}/*** 开始录制* */fun start() {try {prepareEncoder()muxer = MediaMuxer(savePath, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4)virtualDisplay = mediaProjection!!.createVirtualDisplay("$TAG-display",width,height,dpi,DisplayManager.VIRTUAL_DISPLAY_FLAG_PUBLIC,surface,null,null)recordVirtualDisplay()} finally {release()}}private fun recordVirtualDisplay() {while (!isStop) {val index = encoder!!.dequeueOutputBuffer(bufferInfo, TIMEOUT_US.toLong())if (index == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {resetOutputFormat()} else if (index == MediaCodec.INFO_TRY_AGAIN_LATER) {//Log.d(TAG, "retrieving buffers time out!");//delay(10)} else if (index >= 0) {check(muxerStarted) { "MediaMuxer dose not call addTrack(format) " }encodeToVideoTrack(index)encoder!!.releaseOutputBuffer(index, false)}}}private fun encodeToVideoTrack(index: Int) {var encodedData = encoder!!.getOutputBuffer(index)if (bufferInfo.flags and MediaCodec.BUFFER_FLAG_CODEC_CONFIG != 0) {bufferInfo.size = 0}if (bufferInfo.size == 0) {encodedData = null}if (encodedData != null) {encodedData.position(bufferInfo.offset)encodedData.limit(bufferInfo.offset + bufferInfo.size)muxer!!.writeSampleData(videoTrackIndex, encodedData, bufferInfo)}}private fun resetOutputFormat() {check(!muxerStarted) { "output format already changed!" }val newFormat = encoder!!.outputFormatvideoTrackIndex = muxer!!.addTrack(newFormat)muxer!!.start()muxerStarted = true}private fun prepareEncoder() {val format = MediaFormat.createVideoFormat(MIME_TYPE, width, height)format.setInteger(MediaFormat.KEY_COLOR_FORMAT,MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface)format.setInteger(MediaFormat.KEY_BIT_RATE, BIT_RATE)format.setInteger(MediaFormat.KEY_FRAME_RATE, frameRate)format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, IFRAME_INTERVAL)encoder = MediaCodec.createEncoderByType(MIME_TYPE)encoder!!.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE)surface = encoder!!.createInputSurface()encoder!!.start()}private fun release() {if (encoder != null) {encoder!!.stop()encoder!!.release()encoder = null}if (virtualDisplay != null) {virtualDisplay!!.release()}mediaProjection?.stop()if (muxer != null) {muxer?.stop()muxer?.release()muxer = null}}companion object {private const val TAG = "el, In ScreenRecorder"private const val MIME_TYPE = "video/avc" // H.264 Advanced Video Codingprivate const val IFRAME_INTERVAL = 10 // 10 seconds between I-framesprivate const val BIT_RATE = 6000000private const val TIMEOUT_US = 10000}
}

在这个类中,接收以下构造参数:

  • width: Int, 创建虚拟屏幕以及写入的视频宽度
  • height: Int, 创建虚拟屏幕以及写入的视频高度
  • frameRate: Int, 写入的视频帧率
  • dpi: Int, 创建虚拟屏幕的 DPI
  • mediaProjection: MediaProjection?, 用于创建虚拟屏幕的 mediaProjection
  • savePath: String, 写入的视频文件路径

我们可以通过调用 start() 方法开始录屏;调用 stop() 方法停止录屏。

调用 start() 后,会首先调用 prepareEncoder() 方法。该方法主要用途是按照给定参数创建 MediaCodec ,并通过 encoder!!.createInputSurface() 创建一个 Surface 以供后续接收虚拟屏幕的图像数据。

预先设置完成后,按照给定路径创建 MediaMuxer;将参数和之前创建的 surface 传入,创建一个新的虚拟屏幕,并开始接受图像数据。

最后,循环从上面创建的 MediaCodec 中逐帧读出有效图像数据并写入 MediaMuxer 中,即写入视频文件中。

看起来可能比较绕,但是理清楚之后还是非常简单的。

接下来就是如何去调用这个帮助类。

在调用之前,我们需要预先准备好需要的参数:

val savePath = File(externalCacheDir, "${System.currentTimeMillis()}.mp4").absolutePath
val screenSize = getScreenSize()
val mediaProjection = getMediaProjection()
  • savePath 表示写入的视频文件路径,这里我偷懒直接写成了 APP 的缓存目录,如果想要导出到其他地方,记得处理好运行时权限。
  • screenSize 表示的是当前设备的屏幕尺寸
  • mediaProjection 表示请求权限后获取到的权限“令牌”

getScreenSize() 中,我获取了设备的屏幕分辨率:

private fun getScreenSize(): IntSize {val windowManager = getSystemService(WINDOW_SERVICE) as WindowManagerval screenHeight = windowManager.currentWindowMetrics.bounds.height()val screenWidth = windowManager.currentWindowMetrics.bounds.width()return IntSize(screenWidth, screenHeight)
}

但是如果我直接把这个分辨率传给帮助类创建 MediaCodec 的话会报错:

java.lang.IllegalArgumentExceptionat android.media.MediaCodec.native_configure(Native Method)at android.media.MediaCodec.configure(MediaCodec.java:2214)at android.media.MediaCodec.configure(MediaCodec.java:2130)

不过,这个问题只在某些分辨率较高的设备上出现,猜测是不支持高分辨率视频写入吧,所以我实际上使用时是直接写死一个较小的分辨率,而不是使用设备的分辨率。

然后,在 getMediaProjection() 中,我们通过申请到的权限令牌生成 MediaProjection

private fun getMediaProjection(): MediaProjection? {if (resultData == null) {Toast.makeText(this, "未初始化!", Toast.LENGTH_SHORT).show()} else {try {val mediaProjectionManager = getSystemService(MEDIA_PROJECTION_SERVICE) as MediaProjectionManagerreturn mediaProjectionManager.getMediaProjection(Activity.RESULT_OK, resultData!!)} catch (e: IllegalStateException) {Log.e(TAG, "getMediaProjection: ", e)Toast.makeText(this, "ERR: ${e.stackTraceToString()}", Toast.LENGTH_LONG).show()}catch (e: NullPointerException) {Log.e(TAG, "getMediaProjection: ", e)}catch (tr: Throwable) {Log.e(TAG, "getMediaProjection: ", tr)Toast.makeText(this, "ERR: ${tr.stackTraceToString()}", Toast.LENGTH_LONG).show()}}return null
}

最后,通过上面生成的这两个参数初始化录屏帮助类,然后调用 start()

// 这里如果直接使用屏幕尺寸会报错 java.lang.IllegalArgumentException
recorder = ScreenRecorder(886, // screenSize.width,1920, // screenSize.height,24,1,mediaProjection,savePath
)CoroutineScope(Dispatchers.IO).launch {try {recorder.start()} catch (tr: Throwable) {Log.e(TAG, "startScreenRecorder: ", tr)recorder.stop()withContext(Dispatchers.Main) {Toast.makeText(this@OverlayService, "录制失败", Toast.LENGTH_LONG).show()}}
}

这里我把开始录屏放到了协程中,实际上由于我们的程序是运行在 Server 中,所以并不是必须在协程中运行。

总结

自此,在安卓中录屏的方法已经全部介绍完毕。

实际上,同样的原理我们也可以用于实现截图。

截图和录屏不同的地方在于,创建虚拟屏幕时改为使用 ImageReader 创建,然后就可以从 ImageReader 获取到 Bitmap。

最后附上完整的 demo 地址: ScreenRecord

http://www.mmbaike.com/news/27928.html

相关文章:

  • 广州有什么好玩的游乐场seo独立站
  • 响应式网站区别外包
  • 上海网站建设服务分录企业老板培训课程
  • 优书网所有书单哈尔滨优化网站方法
  • wordpress朗读文章学seo需要学什么专业
  • 海口企业做网站设计潍坊百度网站排名
  • 茂名疫情最新消息seo排名点击 seo查询
  • 互联网公司介绍廊坊自动seo
  • 赛罕区城乡建设局网站企业建站要多少钱
  • 蚌埠做网站的公司哪家好营销比较好的知名公司有哪些
  • 北京网站建设华大谷歌浏览器安卓版
  • 昆明网站建设兼职代写平台
  • 网站建站建设联系电话seo的工作原理
  • 建设个网站seo的搜索排名影响因素有哪些
  • 做网站服务好百度账号人工客服
  • 自己怎么做VIP视频解网站如何做优化排名
  • angularjs 做电商网站北京seo排名服务
  • 珠海建站模板媒体发稿费用
  • 太原企业网站seo西安seo技术
  • 对网页设计作品的意见seo综合查询怎么用
  • 做网站的哪里便宜免费访问国外网站的app
  • 网站源码模块2023年小学生简短小新闻
  • 北京做的比较好的网站公司上海关键词优化推荐
  • 怎么做qq代刷网站雷神代刷网站推广
  • 私人信息调查网站google搜索优化
  • dedecms三合一网站源码网上怎么免费推广
  • 济南网站建设哪家好留手机号广告
  • 网站做下载页面免费seo网站自动推广
  • 做信息网站能挣钱吗saas建站
  • 电商发展新方向长沙关键词优化推荐