草庐IT

Android音视频全面介绍与代码实践(一)

徐福记456 2023-04-09 原文

Android在应用层提供丰富的音视频多媒体接口,包括MediaPlayer、MediaCodec、AudioTrack、MediaMuxer、MediaExtractor、MediaRecorder、MediaMetadataRetriever、AudioRecord、AudioManager、Camera/Camera2/CameraX等。本文对咱们常用的多媒体API进行介绍,并且结合代码实例,希望能让大家对Android多媒体有深刻认识。

目录

一、MediaPlayer播放器

1、播放状态图

2、初始化播放器

3、监听播放状态

4、获取音视频信息

5、播放操作

二、MediaExtractor解封装器

三、MediaCodec硬编解码

1、MediaCodec状态图

2、创建MediaCodec

3、同步与异步编解码

4、参数配置

5、编解码队列

6、视频解码示例

四、MediaMuxer封装器

五、AudioTrack音频播放


一、MediaPlayer播放器

MediaPlayer是Android提供的多媒体播放器,支持播放音频和视频,可监听播放状态,可获取音视频信息,支持播放常规操作。详情请查看官方文档:MediaPlayer文档

1、播放状态图

MediaPlayer播放状态包括:Idle、Initialized、Preparing、Prepared、Started、Paused、Stopped、PlaybackCompleted、End、Error。有严格时序和状态转换,需要按照时序来调用播放接口,如下图所示:

2、初始化播放器

初始化播放器步骤:创建播放器、设置DataSource、设置显示Surface、设置播放状态监听、准备播放。代码如下:

    fun initPlayer(filePath: String, surface: Surface) {
        try {
            renderFirstFrame = false
            mediaPlayer = MediaPlayer()
            mediaPlayer!!.setDataSource(filePath)
            mediaPlayer!!.setSurface(surface)
            // 监听播放状态
            setListener()
            mediaPlayer!!.prepareAsync()
        } catch (e: IOException) {
            e.printStackTrace()
        }
    }

3、监听播放状态

播放状态包括:准备完毕、播放信息、缓冲进度、字幕更新、播放出错、播放完成等。有同步初始化和异步初始化两种方式,一般采用异步初始化,在准备完毕onPrepared时开始播放;监听InfoListener为MEDIA_INFO_VIDEO_RENDERING_START时,说明视频渲染第一帧;监听到播放出错onError时,应该结束播放。相关的播放状态监听如下:

    private fun setListener() {
        mediaPlayer!!.setOnPreparedListener {
            mediaPlayer!!.start()
            playerCallback?.onPrepare()
        }

        mediaPlayer!!.setOnInfoListener { mp: MediaPlayer?, what: Int, extra: Int ->
            (
                    if (what == MediaPlayer.MEDIA_INFO_VIDEO_RENDERING_START) {
                        if (!renderFirstFrame) {
                            renderFirstFrame = true
                            playerCallback?.onRenderFirstFrame()
                        }
                    })
            return@setOnInfoListener true
        }

        mediaPlayer!!.setOnBufferingUpdateListener { mp, percent ->
            Log.i("MediaPlayer", "buffer percent=$percent")
        }

        mediaPlayer!!.setOnTimedTextListener { mp: MediaPlayer?, text: TimedText? ->
            Log.i("MediaPlayer", "subtitle=" + text?.text)
        }

        mediaPlayer!!.setOnErrorListener { mp: MediaPlayer?, what: Int, extra: Int ->
            return@setOnErrorListener playerCallback?.onError(what, extra)!!
        }

        mediaPlayer!!.setOnCompletionListener {
            playerCallback?.onCompleteListener()
        }
    }

4、获取音视频信息

在onPrepared回调后,可以获取视频宽高、时长等信息。在播放过程中,可以获取当前播放位置。相关代码如下:

    // 当前播放位置
    fun currentPosition(): Int {
        if (mediaPlayer == null)
            return 0
        return mediaPlayer!!.currentPosition
    }
    // 播放时长
    fun duration(): Int {
        if (mediaPlayer == null)
            return 0
        return mediaPlayer!!.duration
    }
    // 视频宽
    fun getVideoWidth(): Int {
        return mediaPlayer!!.videoWidth
    }
    // 视频高
    fun getVideoHeight(): Int {
        return mediaPlayer!!.videoHeight
    }

5、播放操作

播放操作包括:seek拖动、播放/暂停、静音播放、倍速播放、设置音频、切换音轨等。其中,切换音轨或字幕轨,前提是存在多音轨或多字幕轨。相关代码如下:

    // seek拖动
    fun seekTo(position: Int) {
        mediaPlayer?.seekTo(position)
    }
    // 播放/暂停
    fun togglePlay() {
        if (mediaPlayer!!.isPlaying) {
            mediaPlayer!!.pause()
        } else {
            mediaPlayer!!.start()
        }
    }
    // 静音播放
    fun mute() {
        mediaPlayer?.setVolume(0.0f, 0.0f)
    }
    // 播放音量
    fun setVolume(volume: Float) {
        if (volume < 0 || volume > 1)
            return
        mediaPlayer?.setVolume(volume, volume)
    }

    // 倍速播放
    fun setSpeed(speed: Float) {
        if (speed <= 0 || speed > 8)
            return
        if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) {
            val params = PlaybackParams()
            params.speed = speed
            mediaPlayer?.playbackParams = params
        }
    }
    // 切换音轨或字幕轨
    fun selectTrack(trackId: Int) {
        mediaPlayer?.selectTrack(trackId)
    }

二、MediaExtractor解封装器

MediaExtractor用于媒体文件的解封装,解析媒体信息、获取音视频流。比如,播放视频流程:解封装——>解码——>渲染播放。示例代码如下:

  MediaExtractor extractor = new MediaExtractor();
  extractor.setDataSource(...);
  int numTracks = extractor.getTrackCount();
  for (int i = 0; i < numTracks; ++i) {
    MediaFormat format = extractor.getTrackFormat(i);
    String mime = format.getString(MediaFormat.KEY_MIME);
    if (weAreInterestedInThisTrack) {
      extractor.selectTrack(i);
    }
  }
  ByteBuffer inputBuffer = ByteBuffer.allocate(...)
  while (extractor.readSampleData(inputBuffer, ...) >= 0) {
    int trackIndex = extractor.getSampleTrackIndex();
    long presentationTimeUs = extractor.getSampleTime();
    ...
    extractor.advance();
  }
 
  extractor.release();
  extractor = null;

由此可见,MediaExtractor使用步骤如下:

  1. 创建MediaExtractor解封装器;
  2. 设置DataSource数据源;
  3. 遍历所有媒体轨道,根据MediaFormat的mimetype选择轨道;
  4. 调用readSampleData读取音视频的数据包,调用advance更新;
  5. 调用release释放资源;

三、MediaCodec硬编解码

Android提供MediaCodec进行硬编码、硬解码,包括硬件芯片厂商的硬编解码、系统内置的软编解码。MediaCodec的效率比FFmpeg软解效率高,速度快,占用CPU少。但是FFmpeg软解的兼容性好,支持更广泛的格式和参数。详情请看文档:MediaCodec

1、MediaCodec状态图

MediaCodec的先后顺序状态包括:Unitialized、Configured、Flushed、Running、End of Stream、(Error)、Released。其中,如果编解码过程中出错,会转移到Error状态。具体的状态迁移如下图所示:

2、创建MediaCodec

Android提供两种方式创建MediaCodec,根据name和type。示例代码如下:

    // 根据名字创建编解码
    MediaCodec.createByCodecName(name)
    // 根据mimetype创建编码器
    MediaCodec.createEncoderByType(mimeType)
    // 根据mimetype创建解码器
    MediaCodec.createDecoderByType(mimeType)

3、同步与异步编解码

MediaCodec以前使用同步方式进行编解码。虽然同步调用比较方便,但是会导致阻塞。示例代码如下:

int inputBufferId = codec.dequeueInputBuffer(timeoutUs);
  if (inputBufferId >= 0) {
    ByteBuffer inputBuffer = codec.getInputBuffer(inputBufferId);
    // 填充待编码/解码数据
    ......
    codec.queueInputBuffer(inputBufferId, ...);
  }
  int outputBufferId = codec.dequeueOutputBuffer(...);
  if (outputBufferId >= 0) {
    ByteBuffer outputBuffer = codec.getOutputBuffer(outputBufferId);
    codec.releaseOutputBuffer(outputBufferId, ...);
  }

在Android5.0后,提供异步方式进行编解码,通过设置回调监听实现。示例代码如下:

codec.setCallback(new MediaCodec.Callback() {
  @Override
  void onInputBufferAvailable(MediaCodec mc, int inputBufferId) {
    ByteBuffer inputBuffer = codec.getInputBuffer(inputBufferId);
    // 填充待编码/解码数据
    ......
    codec.queueInputBuffer(inputBufferId, ...);
  }
 
  @Override
  void onOutputBufferAvailable(MediaCodec mc, int outputBufferId, ...) {
    ByteBuffer outputBuffer = codec.getOutputBuffer(outputBufferId);
    codec.releaseOutputBuffer(outputBufferId, ...);
  }
 
  @Override
  void onOutputFormatChanged(MediaCodec mc, MediaFormat format) {

  }
 
  @Override
  void onError(...) {

  }
 });

4、参数配置

MediaCodec提供一系列参数配置,包括:码率、码率模式、GOP间隔等等。而MediaFormat的参数包括:width、height、frameRate、duration、bitrate、sampleRate、channelCount等。另外,在比较新的API版本有新增参数。

(1) HDR10_PLUS_INFO

在Android10(API 29),新增支持HDR10+,可以设置HDR10+的metadata到mediacodec的输入队列,适用于编解码场景。详情请看文档:PARAMETER_KEY_HDR10_PLUS_INFO

(2) LOW_LATENCY

在Android11(API 30),新增支持低延时解码,如果开启,那么解码器不会在内部缓存多余数据。

5、编解码队列

编解码队列包括:编解码前队列和编解码后队列,以生产者和消费者形式存在。队列是环形缓冲区,会循环使用。从输入端角度,客户端作为生产者,为codec提供编解码前的数据,codec作为消费者取出数据进行编解码;从输出端角度,codec作为生产者,为客户端提供编解码后的数据,客户端作为消费者取出数据去渲染。示意图如下:

6、视频解码示例

以视频解码渲染为例,使用MediaExtractor解封装、MediaCodec解码,并且关联到Surface进行渲染。示例代码如下:

    fun decodeVideo() {
        try {
            // 调用MediaExtractor解析得到MediaFormat
            mediaExtractor!!.setDataSource(mFilePath)
            for (i in 0 until mediaExtractor!!.trackCount) {
                mediaFormat = mediaExtractor!!.getTrackFormat(i)
                mimeType = mediaFormat!!.getString(MediaFormat.KEY_MIME)
                if (mimeType != null && mimeType.startsWith("video/")) {
                    mediaExtractor!!.selectTrack(i)
                    break
                }
            }
            // 创建MediaCodec,配置与启动
            mediaCodec = MediaCodec.createDecoderByType(mimeType)
            mediaCodec!!.configure(mediaFormat, mSurface, null, 0)
            mediaCodec!!.start()

            while (!isRunning) {
                val inputIndex = mediaCodec!!.dequeueInputBuffer(DEQUEUE_TIME)
                if (inputIndex >= 0) {
                    val inputBuffer = mediaCodec!!.getInputBuffer(inputIndex)
                    val sampleSize = mediaExtractor!!.readSampleData(inputBuffer!!, 0)
                    // 待解码数据入队列
                    if (sampleSize < 0) {
                        mediaCodec!!.queueInputBuffer(inputIndex, 0, 0, 0, MediaCodec.BUFFER_FLAG_END_OF_STREAM)
                    } else {
                        mediaCodec!!.queueInputBuffer(inputIndex, 0, sampleSize, mediaExtractor!!.sampleTime, 0)
                        mediaExtractor!!.advance()
                    }
                }
                // 解码后数据出队列
                val outputIndex = mediaCodec!!.dequeueOutputBuffer(bufferInfo, DEQUEUE_TIME)
                if (outputIndex != MediaCodec.INFO_OUTPUT_FORMAT_CHANGED 
                        && outputIndex != MediaCodec.INFO_TRY_AGAIN_LATER
                        && outputIndex != MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) {
                        mediaCodec!!.releaseOutputBuffer(outputIndex, true)
                }
            }
        } catch (e: Exception) {
            Log.e(TAG, "decode error=$e")
        }
    }

四、MediaMuxer封装器

MediaMuxer封装器用于封装音视频流,和MediaExtractor作用刚好相反。比如,录制视频流程:采集音视频流——>编码——>封装。接下来,以MediaExtractor解封装和MediaMuxer封装作为示例,参考代码如下:

   fun muxMediaFile(inputPath: String, outputPath: String): Boolean {
        if (inputPath.isEmpty() || outputPath.isEmpty()) {
            return false
        }
        var happenError = false
        // 1、创建MediaMuxer
        val mediaMuxer = MediaMuxer(outputPath, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4)
        val mediaExtractor = MediaExtractor()
        try {
            var videoIndex = 0
            var audioIndex = 0
            var audioFormat: MediaFormat? = null
            var videoFormat: MediaFormat? = null
            var finished = false
            val bufferInfo = MediaCodec.BufferInfo()
            val inputBuffer = ByteBuffer.allocate(2 * 1024 * 1024)
            mediaExtractor.setDataSource(inputPath)
            // 遍历所有轨道,根据mimetype选择轨道
            for (i in 0 until mediaExtractor.trackCount) {
                val mediaFormat = mediaExtractor.getTrackFormat(i)
                val mimeType = mediaFormat.getString(MediaFormat.KEY_MIME)
                if (mimeType != null && mimeType.startsWith("video")) {
                    videoIndex = i
                    videoFormat = mediaFormat
                    mediaExtractor.selectTrack(i)
                } else if (mimeType != null && mimeType.startsWith("audio") && audioFormat == null) {
                    audioIndex = i
                    audioFormat = mediaFormat
                    mediaExtractor.selectTrack(i)
                }
            }
            // 2、添加轨道,传入MediaFormat
            if (videoFormat != null) {
                mediaMuxer.addTrack(videoFormat)
            }
            if (audioFormat != null) {
                mediaMuxer.addTrack(audioFormat)
            }
            // 3、开启MediaMuxer
            mediaMuxer.start()

            while (!finished) {
                // 解封装获取音视频流数据
                val sampleSize = mediaExtractor.readSampleData(inputBuffer, 0)
                if (sampleSize > 0) {
                    bufferInfo.size = sampleSize
                    bufferInfo.flags = mediaExtractor.sampleFlags
                    bufferInfo.presentationTimeUs = mediaExtractor.sampleTime
                    // 4、调用MediaMuxer把音视频流重新封装
                    if (mediaExtractor.sampleTrackIndex == videoIndex) {
                        mediaMuxer.writeSampleData(videoIndex, inputBuffer, bufferInfo)
                    } else if (mediaExtractor.sampleTrackIndex == audioIndex) {
                        mediaMuxer.writeSampleData(audioIndex, inputBuffer, bufferInfo)
                    }
                    inputBuffer.flip()
                    mediaExtractor.advance()
                } else if (sampleSize < 0) {
                    finished = true
                }
            }

        } catch (e: Exception) {
            happenError = true
        } finally {
            // 5、释放资源
            mediaMuxer.release()
            mediaExtractor.release()
            return !happenError
        }
    }

由此可见,MediaMuxer的使用步骤如下:

  1. 创建MediaMuxer;
  2. 添加MediaFormat到轨道;
  3. 开启MediaMuxer;
  4. 调用writeSampleData来封装音视频流;
  5. 释放资源;

五、AudioTrack音频播放

AudioTrack是Android在应用层提供的音频播放器。如果对延时有严格要求,可以使用底层提供的OpenSL ES,或者AAudio,而oboe库有对AAudio的封装。其中AAudio通过共享内存,降低延时,提高处理效率。

以MediaExtractor解封装、MediaCodec解码、AudioTrack播放三者结合,看看AudioTrack的代码示例。首先是初始化工作:

    // 初始化MediaExtractor
    private fun parseAudioFormat(path: String): MediaFormat? {
        mediaExtractor = MediaExtractor()
        try {
            mediaExtractor?.setDataSource(path)
            for (i in 0 until mediaExtractor!!.trackCount) {
                val mediaFormat = mediaExtractor!!.getTrackFormat(i)
                val mimeType = mediaFormat.getString(MediaFormat.KEY_MIME)
                if (mimeType != null && mimeType.startsWith("audio")) {
                    mediaExtractor!!.selectTrack(i)
                    return mediaFormat
                }
            }
        } catch (e: Exception) {
            Log.e(TAG, "parseAudioFormat err=$e")
        }
        return null
    }
    // 初始化MediaCodec
    private fun initMediaCodec(mediaFormat: MediaFormat): Boolean {
        val mimeType = mediaFormat.getString(MediaFormat.KEY_MIME)
        mediaCodec = MediaCodec.createDecoderByType(mimeType)
        return try {
            mediaCodec!!.configure(mediaFormat, null, null, 0)
            mediaCodec!!.start()
            true
        } catch (e: Exception) {
            Log.e(TAG, "initMediaCodec err=$e")
            false
        }
    }
    // 初始化AudioTrack
    private fun initAudioTrack(mediaFormat: MediaFormat): Boolean {
        val sampleRate = mediaFormat.getInteger(MediaFormat.KEY_SAMPLE_RATE)
        val channelCount = mediaFormat.getInteger(MediaFormat.KEY_CHANNEL_COUNT)
        val channelConfig = if (channelCount == 1) {
            AudioFormat.CHANNEL_OUT_MONO
        } else  {
            AudioFormat.CHANNEL_OUT_STEREO
        }
        val encoding = AudioFormat.ENCODING_PCM_16BIT
        val bufferSize = AudioTrack.getMinBufferSize(sampleRate, channelConfig, encoding)
        Log.e(TAG, "sampleRate=$sampleRate, channelCount=$channelCount, bufferSize=$bufferSize")

        try {
            val audioFormat = AudioFormat.Builder()
                    .setEncoding(encoding)
                    .setSampleRate(sampleRate)
                    .setChannelMask(channelConfig)
                    .build()
            val audioAttributes = AudioAttributes.Builder()
                    .setLegacyStreamType(AudioManager.STREAM_MUSIC)
                    .build()
            audioTrack = AudioTrack(audioAttributes, audioFormat,
                    bufferSize, AudioTrack.MODE_STREAM, AudioManager.AUDIO_SESSION_ID_GENERATE)
            audioTrack!!.play()
        } catch (e: Exception) {
            Log.e(TAG, "initAudioTrack err=$e")
            return false
        }
        return true
    }

接下来,调用MediaExtractor的readSampleData方法解析音视频流,调用MediaCodec的queueInputBuffer和dequeueOutputBuffer方法进行解码,最后调用AudioTrack的write方法进行播放。需要注意的是,解封装、解码、播放应该分为三个线程,这里只是简单演示使用方法。相关代码如下:

    fun playAudio(path: String) {
        var finished = false
        val data = ByteArray(10 * 1024)
        running = AtomicBoolean(true)
        val bufferInfo = MediaCodec.BufferInfo()
        val mediaFormat = parseAudioFormat(path) ?: return release()
        var result = initMediaCodec(mediaFormat)
        if (!result) {
            return release()
        }
        result = initAudioTrack(mediaFormat)
        if (!result) {
            return release()
        }

        while (!finished) {
            if (!running!!.get()) {
                break
            }
            val inputIndex = mediaCodec!!.dequeueInputBuffer(DEQUEUE_TIME)
            if (inputIndex >= 0) {
                val inputBuffer = mediaCodec!!.getInputBuffer(inputIndex)
                // demux
                val sampleSize = mediaExtractor!!.readSampleData(inputBuffer!!, 0)
                // decode
                if (sampleSize < 0) {
                    mediaCodec!!.queueInputBuffer(inputIndex, 0, 0,
                            0, MediaCodec.BUFFER_FLAG_END_OF_STREAM)
                    finished = true
                } else {
                    mediaCodec!!.queueInputBuffer(inputIndex, 0, sampleSize,
                            mediaExtractor!!.sampleTime, mediaExtractor!!.sampleFlags)
                    mediaExtractor!!.advance()
                }
            }

            val outputIndex = mediaCodec!!.dequeueOutputBuffer(bufferInfo, DEQUEUE_TIME)
            // play
            if (outputIndex >= 0) {
                val outputBuffer = mediaCodec!!.getOutputBuffer(outputIndex)
                val size = outputBuffer!!.limit()
                outputBuffer.get(data, outputBuffer.position(), size - outputBuffer.position())
                audioTrack!!.write(data, 0, size)
                mediaCodec!!.releaseOutputBuffer(outputIndex, false)
                SystemClock.sleep(SLEEP_TIME)
            }
        }

        release()
    }

至此,关于Android多媒体的视频播放器、音频播放器、MediaCodec编解码、多媒体封装与解封装器介绍完毕。完整代码与学习音视频,可查看GitHub:FFmpegAndroid

有关Android音视频全面介绍与代码实践(一)的更多相关文章

  1. ruby-on-rails - 使用 Ruby on Rails 进行自动化测试 - 最佳实践 - 2

    很好奇,就使用ruby​​onrails自动化单元测试而言,你们正在做什么?您是否创建了一个脚本来在cron中运行rake作业并将结果邮寄给您?git中的预提交Hook?只是手动调用?我完全理解测试,但想知道在错误发生之前捕获错误的最佳实践是什么。让我们理所当然地认为测试本身是完美无缺的,并且可以正常工作。下一步是什么以确保他们在正确的时间将可能有害的结果传达给您? 最佳答案 不确定您到底想听什么,但是有几个级别的自动代码库控制:在处理某项功能时,您可以使用类似autotest的内容获得关于哪些有效,哪些无效的即时反馈。要确保您的提

  2. ruby - 如何在 buildr 项目中使用 Ruby 代码? - 2

    如何在buildr项目中使用Ruby?我在很多不同的项目中使用过Ruby、JRuby、Java和Clojure。我目前正在使用我的标准Ruby开发一个模拟应用程序,我想尝试使用Clojure后端(我确实喜欢功能代码)以及JRubygui和测试套件。我还可以看到在未来的不同项目中使用Scala作为后端。我想我要为我的项目尝试一下buildr(http://buildr.apache.org/),但我注意到buildr似乎没有设置为在项目中使用JRuby代码本身!这看起来有点傻,因为该工具旨在统一通用的JVM语言并且是在ruby中构建的。除了将输出的jar包含在一个独特的、仅限ruby​​

  3. ruby-on-rails - Rails 源代码 : initialize hash in a weird way? - 2

    在rails源中:https://github.com/rails/rails/blob/master/activesupport/lib/active_support/lazy_load_hooks.rb可以看到以下内容@load_hooks=Hash.new{|h,k|h[k]=[]}在IRB中,它只是初始化一个空哈希。和做有什么区别@load_hooks=Hash.new 最佳答案 查看rubydocumentationforHashnew→new_hashclicktotogglesourcenew(obj)→new_has

  4. ruby-on-rails - 浏览 Ruby 源代码 - 2

    我的主要目标是能够完全理解我正在使用的库/gem。我尝试在Github上从头到尾阅读源代码,但这真的很难。我认为更有趣、更温和的踏脚石就是在使用时阅读每个库/gem方法的源代码。例如,我想知道RubyonRails中的redirect_to方法是如何工作的:如何查找redirect_to方法的源代码?我知道在pry中我可以执行类似show-methodmethod的操作,但我如何才能对Rails框架中的方法执行此操作?您对我如何更好地理解Gem及其API有什么建议吗?仅仅阅读源代码似乎真的很难,尤其是对于框架。谢谢! 最佳答案 Ru

  5. ruby - 模块嵌套代码风格偏好 - 2

    我的假设是moduleAmoduleBendend和moduleA::Bend是一样的。我能够从thisblog找到解决方案,thisSOthread和andthisSOthread.为什么以及什么时候应该更喜欢紧凑语法A::B而不是另一个,因为它显然有一个缺点?我有一种直觉,它可能与性能有关,因为在更多命名空间中查找常量需要更多计算。但是我无法通过对普通类进行基准测试来验证这一点。 最佳答案 这两种写作方法经常被混淆。首先要说的是,据我所知,没有可衡量的性能差异。(在下面的书面示例中不断查找)最明显的区别,可能也是最著名的,是你的

  6. ruby - 寻找通过阅读代码确定编程语言的ruby gem? - 2

    几个月前,我读了一篇关于ruby​​gem的博客文章,它可以通过阅读代码本身来确定编程语言。对于我的生活,我不记得博客或gem的名称。谷歌搜索“ruby编程语言猜测”及其变体也无济于事。有人碰巧知道相关gem的名称吗? 最佳答案 是这个吗:http://github.com/chrislo/sourceclassifier/tree/master 关于ruby-寻找通过阅读代码确定编程语言的rubygem?,我们在StackOverflow上找到一个类似的问题:

  7. ruby - Net::HTTP 获取源代码和状态 - 2

    我目前正在使用以下方法获取页面的源代码:Net::HTTP.get(URI.parse(page.url))我还想获取HTTP状态,而无需发出第二个请求。有没有办法用另一种方法做到这一点?我一直在查看文档,但似乎找不到我要找的东西。 最佳答案 在我看来,除非您需要一些真正的低级访问或控制,否则最好使用Ruby的内置Open::URI模块:require'open-uri'io=open('http://www.example.org/')#=>#body=io.read[0,50]#=>"["200","OK"]io.base_ur

  8. 世界前沿3D开发引擎HOOPS全面讲解——集3D数据读取、3D图形渲染、3D数据发布于一体的全新3D应用开发工具 - 2

    无论您是想搭建桌面端、WEB端或者移动端APP应用,HOOPSPlatform组件都可以为您提供弹性的3D集成架构,同时,由工业领域3D技术专家组成的HOOPS技术团队也能为您提供技术支持服务。如果您的客户期望有一种在多个平台(桌面/WEB/APP,而且某些客户端是“瘦”客户端)快速、方便地将数据接入到3D应用系统的解决方案,并且当访问数据时,在各个平台上的性能和用户体验保持一致,HOOPSPlatform将帮助您完成。利用HOOPSPlatform,您可以开发在任何环境下的3D基础应用架构。HOOPSPlatform可以帮您打造3D创新型产品,HOOPSSDK包含的技术有:快速且准确的CAD

  9. 程序员如何提高代码能力? - 2

    前言作为一名程序员,自己的本质工作就是做程序开发,那么程序开发的时候最直接的体现就是代码,检验一个程序员技术水平的一个核心环节就是开发时候的代码能力。众所周知,程序开发的水平提升是一个循序渐进的过程,每一位程序员都是从“菜鸟”变成“大神”的,所以程序员在程序开发过程中的代码能力也是根据平时开发中的业务实践来积累和提升的。提高代码能力核心要素程序员要想提高自身代码能力,尤其是新晋程序员的代码能力有很大的提升空间的时候,需要针对性的去提高自己的代码能力。提高代码能力其实有几个比较关键的点,只要把握住这些方面,就能很好的、快速的提高自己的一部分代码能力。1、多去阅读开源项目,如有机会可以亲自参与开源

  10. 叮咚买菜基于 Apache Doris 统一 OLAP 引擎的应用实践 - 2

    导读:随着叮咚买菜业务的发展,不同的业务场景对数据分析提出了不同的需求,他们希望引入一款实时OLAP数据库,构建一个灵活的多维实时查询和分析的平台,统一数据的接入和查询方案,解决各业务线对数据高效实时查询和精细化运营的需求。经过调研选型,最终引入ApacheDoris作为最终的OLAP分析引擎,Doris作为核心的OLAP引擎支持复杂地分析操作、提供多维的数据视图,在叮咚买菜数十个业务场景中广泛应用。作者|叮咚买菜资深数据工程师韩青叮咚买菜创立于2017年5月,是一家专注美好食物的创业公司。叮咚买菜专注吃的事业,为满足更多人“想吃什么”而努力,通过美好食材的供应、美好滋味的开发以及美食品牌的孵

随机推荐