草庐IT

FFmpeg源码分析:av_seek_frame()与avformat_seek_file()

徐福记456 2023-04-27 原文

在播放视频过程中,想要跳过中间直接看精彩片段怎么办呢?或者看到精彩片段,想回到某个位置重新观看又该怎么办呢?所以播放器得提供seek操作实现快进快退功能,FFmpeg在libavformat模块提供此功能的API,av_seek_frame()属于旧版API,而avformat_seek_file()属于新版API并且兼容旧版本。

首先,我们来看看av_seek_frame()函数定义,位于libavformat/avformat.h。根据描述,该函数用于移动到指定时间戳的关键帧位置,其定义如下:

/**
 * Seek to the keyframe at timestamp.
 *
 * @param s media file handle
 * @param stream_index If stream_index is (-1), a default
 * stream is selected, and timestamp is automatically converted
 * from AV_TIME_BASE units to the stream specific time_base.
 * @param timestamp Timestamp in AVStream.time_base units
 * @param flags flags which select direction and seeking mode
 * @return >= 0 on success
 */
int av_seek_frame(AVFormatContext *s, int stream_index, int64_t timestamp, int flags);

然后对比一下,新版本API的avformat_seek_file()函数定义,同样位于libavformat/avformat.h。根据描述,移动到时间戳最近邻的位置(在min_ts与max_ts范围内),其定义如下:

/**
 * Seek to timestamp ts.
 * Seeking will be done so that the point from which all active streams
 * can be presented successfully will be closest to ts and within min/max_ts.
 *
 * If flags contain AVSEEK_FLAG_BYTE, then all timestamps are in bytes.
 * If flags contain AVSEEK_FLAG_FRAME, then all timestamps are in frames.
 * If flags contain AVSEEK_FLAG_ANY, then non-keyframes are treated as keyframes.
 * If flags contain AVSEEK_FLAG_BACKWARD, it is ignored.
 *
 * @param s media file handle
 * @param stream_index index of the stream which is used as time base reference
 * @param min_ts smallest acceptable timestamp
 * @param ts target timestamp
 * @param max_ts largest acceptable timestamp
 * @param flags flags
 * @return >=0 on success, error code otherwise
 */
int avformat_seek_file(AVFormatContext *s, int stream_index, 
int64_t min_ts, int64_t ts, int64_t max_ts, int flags);

上面提及的flag是seek模式,总共有4种模式,声明也是位于libavformat/avformat.h。AVSEEK_FLAG_BACKWARD是往回seek,即seek到时间戳的上一个关键帧;AVSEEK_FLAG_BYTE是以字节数方式seek;AVSEEK_FLAG_ANY支持seek到任意音视频帧,包括非关键帧,即精准seek,会比较耗时;AVSEEK_FLAG_FRAME是以帧数量方式seek。具体描述如下:

#define AVSEEK_FLAG_BACKWARD 1 ///< seek backward
#define AVSEEK_FLAG_BYTE     2 ///< seeking based on position in bytes
#define AVSEEK_FLAG_ANY      4 ///< seek to any frame, even non-keyframes
#define AVSEEK_FLAG_FRAME    8 ///< seeking based on frame number

 avformat_seek_file()函数调用流程如下:

1、avformat_seek_file

avformat_seek_file函数优先调用read_seek2函数,如果不支持read_seek2就回退到旧版的av_seek_frame函数。具体调用流程如下:

int avformat_seek_file(AVFormatContext *s, int stream_index, int64_t min_ts,
                       int64_t ts, int64_t max_ts, int flags)
{
    if (min_ts > ts || max_ts < ts)
        return -1;
    if (stream_index < -1 || stream_index >= (int)s->nb_streams)
        return AVERROR(EINVAL);
    if (s->seek2any>0)
        flags |= AVSEEK_FLAG_ANY;
    flags &= ~AVSEEK_FLAG_BACKWARD;
    // 优先调用read_seek2
    if (s->iformat->read_seek2) {
        int ret;
        ff_read_frame_flush(s);
        if (stream_index == -1 && s->nb_streams == 1) {
            AVRational time_base = s->streams[0]->time_base;
            ts = av_rescale_q(ts, AV_TIME_BASE_Q, time_base);
            min_ts = av_rescale_rnd(min_ts, time_base.den,
                                    time_base.num * (int64_t)AV_TIME_BASE,
                                    AV_ROUND_UP   | AV_ROUND_PASS_MINMAX);
            max_ts = av_rescale_rnd(max_ts, time_base.den,
                                    time_base.num * (int64_t)AV_TIME_BASE,
                                    AV_ROUND_DOWN | AV_ROUND_PASS_MINMAX);
            stream_index = 0;
        }
        ret = s->iformat->read_seek2(s, stream_index, min_ts,
                                     ts, max_ts, flags);
        if (ret >= 0)
            ret = avformat_queue_attached_pictures(s);
        return ret;
    }

    // 回退旧版API,使用av_seek_frame
    if (s->iformat->read_seek || 1) {
        int dir = (ts - (uint64_t)min_ts > (uint64_t)max_ts - ts ? AVSEEK_FLAG_BACKWARD : 0);
        int ret = av_seek_frame(s, stream_index, ts, flags | dir);
        if (ret<0 && ts != min_ts && max_ts != ts) {
            ret = av_seek_frame(s, stream_index, dir ? max_ts : min_ts, flags | dir);
            if (ret >= 0)
                ret = av_seek_frame(s, stream_index, ts, flags | (dir^AVSEEK_FLAG_BACKWARD));
        }
        return ret;
    }
    return -1;
}

2、read_seek2与read_seek

read_seek2和read_seek属于AVInputFormat结构体的函数指针。以mp4封装格式为例(位于libavformat/mov.c),对应的AVInputFormat结构体如下:

AVInputFormat ff_mov_demuxer = {
    .name           = "mov,mp4,m4a,3gp,3g2,mj2",
    .long_name      = NULL_IF_CONFIG_SMALL("QuickTime / MOV"),
    .priv_class     = &mov_class,
    .priv_data_size = sizeof(MOVContext),
    .extensions     = "mov,mp4,m4a,3gp,3g2,mj2,psp,m4b,ism,ismv,isma,f4v",
    .read_probe     = mov_probe,
    .read_header    = mov_read_header,
    .read_packet    = mov_read_packet,
    .read_close     = mov_read_close,
    .read_seek      = mov_read_seek,
    .flags          = AVFMT_NO_BYTE_SEEK | AVFMT_SEEK_TO_PTS,
};

 由此可见,read_seek2的函数还没有实现,而read_seek函数指针指向mov_read_seek()。具体实现如下:

static int mov_read_seek(AVFormatContext *s, int stream_index, int64_t sample_time, int flags)
{
    MOVContext *mc = s->priv_data;
    AVStream *st;
    int sample;
    int i;

    if (stream_index >= s->nb_streams)
        return AVERROR_INVALIDDATA;

    st = s->streams[stream_index];
    sample = mov_seek_stream(s, st, sample_time, flags);
    if (sample < 0)
        return sample;

    if (mc->seek_individually) {
        /* adjust seek timestamp to found sample timestamp */
        int64_t seek_timestamp = st->index_entries[sample].timestamp;
        st->internal->skip_samples = mov_get_skip_samples(st, sample);

        for (i = 0; i < s->nb_streams; i++) {
            int64_t timestamp;
            st = s->streams[i];
            if (stream_index == i)
                continue;
            timestamp = av_rescale_q(seek_timestamp, 
			    s->streams[stream_index]->time_base, st->time_base);
            sample = mov_seek_stream(s, st, timestamp, flags);
            if (sample >= 0)
                st->internal->skip_samples = mov_get_skip_samples(st, sample);
        }
    } else {
        for (i = 0; i < s->nb_streams; i++) {
            MOVStreamContext *sc;
            st = s->streams[i];
            sc = st->priv_data;
            mov_current_sample_set(sc, 0);
        }
        while (1) {
            MOVStreamContext *sc;
            AVIndexEntry *entry = mov_find_next_sample(s, &st);
            if (!entry)
                return AVERROR_INVALIDDATA;
            sc = st->priv_data;
            if (sc->ffindex == stream_index && sc->current_sample == sample)
                break;
            mov_current_sample_inc(sc);
        }
    }
    return 0;
}

3、av_seek_frame

av_seek_frame函数首先判断是否存在iformat->read_seek2,如果存在就调用对应read_seek2(),如果不存在则调用seek_frame_internal()去执行seek操作:

int av_seek_frame(AVFormatContext *s, int stream_index,
                  int64_t timestamp, int flags)
{
    int ret;
    // 如果存在read_seek2的API,调用avformat_seek_file
    if (s->iformat->read_seek2 && !s->iformat->read_seek) {
        int64_t min_ts = INT64_MIN, max_ts = INT64_MAX;
        if ((flags & AVSEEK_FLAG_BACKWARD))
            max_ts = timestamp;
        else
            min_ts = timestamp;
        return avformat_seek_file(s, stream_index, min_ts, timestamp, max_ts,
                                  flags & ~AVSEEK_FLAG_BACKWARD);
    }
    // 调用内部的寻帧方法
    ret = seek_frame_internal(s, stream_index, timestamp, flags);

    if (ret >= 0)
        ret = avformat_queue_attached_pictures(s);

    return ret;
}

4、seek_frame_internal

接下来我们看看seek_frame_internal函数实现:

static int seek_frame_internal(AVFormatContext *s, int stream_index,
                               int64_t timestamp, int flags)
{
    int ret;
    AVStream *st;
    // 以字节数方式寻帧
    if (flags & AVSEEK_FLAG_BYTE) {
        if (s->iformat->flags & AVFMT_NO_BYTE_SEEK)
            return -1;
        ff_read_frame_flush(s);
        return seek_frame_byte(s, stream_index, timestamp, flags);
    }
    if (stream_index < 0) {
        stream_index = av_find_default_stream_index(s);
        if (stream_index < 0)
            return -1;
        st = s->streams[stream_index];
        timestamp = av_rescale(timestamp, st->time_base.den,
                               AV_TIME_BASE * (int64_t) st->time_base.num);
    }
    // 以指定方式寻帧
    if (s->iformat->read_seek) {
        ff_read_frame_flush(s);
        ret = s->iformat->read_seek(s, stream_index, timestamp, flags);
    } else
        ret = -1;
    if (ret >= 0)
        return 0;

    if (s->iformat->read_timestamp &&
        !(s->iformat->flags & AVFMT_NOBINSEARCH)) {
        ff_read_frame_flush(s);
		// 以二分查找方式寻帧
        return ff_seek_frame_binary(s, stream_index, timestamp, flags);
    } else if (!(s->iformat->flags & AVFMT_NOGENSEARCH)) {
        ff_read_frame_flush(s);
		// 以通用方式寻帧
        return seek_frame_generic(s, stream_index, timestamp, flags);
    } else
        return -1;
}

由此可见,该函数按照顺序有4个执行步骤:

  1. 以字节数方式寻帧;
  2. 以指定方式read_seek寻帧;
  3. 以二分查找方式寻帧;
  4. 以通用方式寻帧;

5、seek_frame_byte

我们看看byte方式的seek操作,直接调用avio_seek移动到指定位置:

static int seek_frame_byte(AVFormatContext *s, int stream_index,
                           int64_t pos, int flags)
{
    int64_t pos_min, pos_max;
    pos_min = s->internal->data_offset;
    pos_max = avio_size(s->pb) - 1;

    if (pos < pos_min)
        pos = pos_min;
    else if (pos > pos_max)
        pos = pos_max;

    avio_seek(s->pb, pos, SEEK_SET);
    s->io_repositioned = 1;

    return 0;
}

6、ff_seek_frame_binary

再看看二分查找方式的seek操作:

int ff_seek_frame_binary(AVFormatContext *s, int stream_index,
                         int64_t target_ts, int flags)
{
    const AVInputFormat *avif = s->iformat;
    int64_t av_uninit(pos_min), av_uninit(pos_max), pos, pos_limit;
    int64_t ts_min, ts_max, ts;
    int index;
    int64_t ret;
    AVStream *st;

    if (stream_index < 0)
        return -1;
    st = s->streams[stream_index];
    if (st->index_entries) {
        AVIndexEntry *e;
		// 根据时间戳找到对应的音视频帧索引
        index = av_index_search_timestamp(st, target_ts, flags | AVSEEK_FLAG_BACKWARD);
        index = FFMAX(index, 0);
        e     = &st->index_entries[index];

        if (e->timestamp <= target_ts || e->pos == e->min_distance) {
            pos_min = e->pos;
            ts_min  = e->timestamp;
        } else {
            av_assert1(index == 0);
        }

        index = av_index_search_timestamp(st, target_ts, flags & ~AVSEEK_FLAG_BACKWARD);
        if (index >= 0) {
            e = &st->index_entries[index];
            av_assert1(e->timestamp >= target_ts);
            pos_max   = e->pos;
            ts_max    = e->timestamp;
            pos_limit = pos_max - e->min_distance;
        }
    }
    // 根据时间戳搜索对应position
    pos = ff_gen_search(s, stream_index, target_ts, pos_min, pos_max, pos_limit,
                        ts_min, ts_max, flags, &ts, avif->read_timestamp);
    if (pos < 0)
        return -1;
    // 调用avio_seek跳转到指定position
    if ((ret = avio_seek(s->pb, pos, SEEK_SET)) < 0)
        return ret;
    ff_read_frame_flush(s);
    ff_update_cur_dts(s, st, ts);

    return 0;
}

由此可见, ff_seek_frame_binary函数主要有3个步骤:

  • 调用av_index_search_timestamp()根据时间戳寻找数组索引;
  • 根据时间戳搜索对应position;
  • 调用avio_seek跳转到指定position;

 其中,av_index_search_timestamp函数又调用ff_index_search_timestamp函数,如下所示:

int av_index_search_timestamp(AVStream *st, int64_t wanted_timestamp, int flags)
{
    return ff_index_search_timestamp(st->index_entries, st->nb_index_entries,
                                     wanted_timestamp, flags);
}

 我们来看看ff_index_search_timestamp函数的具体实现,主要用二分查找法根据指定时间戳查找数组对应的下标索引:

int ff_index_search_timestamp(const AVIndexEntry *entries, int nb_entries,
                              int64_t wanted_timestamp, int flags)
{
    int a, b, m;
    int64_t timestamp;
    a = -1;
    b = nb_entries;

    if (b && entries[b - 1].timestamp < wanted_timestamp)
        a = b - 1;
    // 二分查找法
    while (b - a > 1) {
        m = (a + b) >> 1;
        while ((entries[m].flags & AVINDEX_DISCARD_FRAME) 
			&& m < b && m < nb_entries - 1) {
            m++;
            if (m == b && entries[m].timestamp >= wanted_timestamp) {
                m = b - 1;
                break;
            }
        }
        timestamp = entries[m].timestamp;
        if (timestamp >= wanted_timestamp)
            b = m;
        if (timestamp <= wanted_timestamp)
            a = m;
    }
    m = (flags & AVSEEK_FLAG_BACKWARD) ? a : b;
    if (!(flags & AVSEEK_FLAG_ANY))
        while (m >= 0 && m < nb_entries &&
               !(entries[m].flags & AVINDEX_KEYFRAME))
            m += (flags & AVSEEK_FLAG_BACKWARD) ? -1 : 1;

    if (m == nb_entries)
        return -1;
    return m;
}

 而ff_gen_search()函数使用3种查找方式:插值查找、二分查找、线性查找。具体如下:

int64_t ff_gen_search(AVFormatContext *s, int stream_index, int64_t target_ts,
                      int64_t pos_min, int64_t pos_max, int64_t pos_limit,
                      int64_t ts_min, int64_t ts_max,
                      int flags, int64_t *ts_ret,
                      int64_t (*read_timestamp)(struct AVFormatContext *, int,
                                                int64_t *, int64_t))
{
    ......

    while (pos_min < pos_limit) {
        if (no_change == 0) {
            int64_t approximate_keyframe_distance = pos_max - pos_limit;
            // 插值查找
            pos = av_rescale(target_ts - ts_min, pos_max - pos_min,
                             ts_max - ts_min) +
                  pos_min - approximate_keyframe_distance;
        } else if (no_change == 1) {
            // 二分查找
            pos = (pos_min + pos_limit) >> 1;
        } else {
            // 线性查找
            pos = pos_min;
        }
        ......
        // 读取时间戳
        ts = ff_read_timestamp(s, stream_index, &pos, INT64_MAX, read_timestamp);
        ......
    }

    pos     = (flags & AVSEEK_FLAG_BACKWARD) ? pos_min : pos_max;
    ts      = (flags & AVSEEK_FLAG_BACKWARD) ? ts_min  : ts_max;
    *ts_ret = ts;
    return pos;
}

7、seek_frame_generic

 最后看看seek_frame_generic函数实现:

static int seek_frame_generic(AVFormatContext *s, int stream_index,
                              int64_t timestamp, int flags)
{
    int index;
    int64_t ret;
    AVStream *st;
    AVIndexEntry *ie;
    st = s->streams[stream_index];
    // 根据时间戳找到对应的音视频帧索引
    index = av_index_search_timestamp(st, timestamp, flags);

    if (index < 0 && st->nb_index_entries &&
        timestamp < st->index_entries[0].timestamp)
        return -1;

    if (index < 0 || index == st->nb_index_entries - 1) {
        AVPacket *pkt = s->internal->pkt;
        int nonkey = 0;
        // 调用avio_seek跳转到指定position
        if (st->nb_index_entries) {
            av_assert0(st->index_entries);
            ie = &st->index_entries[st->nb_index_entries - 1];
            if ((ret = avio_seek(s->pb, ie->pos, SEEK_SET)) < 0)
                return ret;
            ff_update_cur_dts(s, st, ie->timestamp);
        } else {
            if ((ret = avio_seek(s->pb, s->internal->data_offset, SEEK_SET)) < 0)
                return ret;
        }
        av_packet_unref(pkt);
        for (;;) {
            int read_status;
            do {
                read_status = av_read_frame(s, pkt);
            } while (read_status == AVERROR(EAGAIN));
            if (read_status < 0)
                break;
            if (stream_index == pkt->stream_index && pkt->dts > timestamp) {
                if (pkt->flags & AV_PKT_FLAG_KEY) {
                    av_packet_unref(pkt);
                    break;
                }
                if (nonkey++ > 1000 && st->codecpar->codec_id != AV_CODEC_ID_CDGRAPHICS) {
                    av_packet_unref(pkt);
                    break;
                }
            }
            av_packet_unref(pkt);
        }
        index = av_index_search_timestamp(st, timestamp, flags);
    }
    if (index < 0)
        return -1;

    ff_read_frame_flush(s);
    if (s->iformat->read_seek)
        if (s->iformat->read_seek(s, stream_index, timestamp, flags) >= 0)
            return 0;
    ie = &st->index_entries[index];
    if ((ret = avio_seek(s->pb, ie->pos, SEEK_SET)) < 0)
        return ret;
	// 更新当前dts
    ff_update_cur_dts(s, st, ie->timestamp);

    return 0;
}

seek_frame_generic函数主要有3个步骤:

  • 调用av_index_search_timestamp()根据时间戳寻找数组索引;
  • 调用avio_seek跳转到指定position;
  • 调用ff_update_cur_dts()更新当前dts;

至此,av_seek_frame()和avformat_seek_file()函数分析完毕。

有关FFmpeg源码分析:av_seek_frame()与avformat_seek_file()的更多相关文章

  1. ruby-on-rails - rails : save file from URL and save it to Amazon S3 - 2

    从给定URL下载文件并立即将其上传到AmazonS3的更直接的方法是什么(+将有关文件的一些信息保存到数据库中,例如名称、大小等)?现在,我既不使用Paperclip,也不使用Carrierwave。谢谢 最佳答案 简单明了:require'open-uri'require's3'amazon=S3::Service.new(access_key_id:'KEY',secret_access_key:'KEY')bucket=amazon.buckets.find('image_storage')url='http://www.ex

  2. ruby - 无法让 RSpec 工作—— 'require' : cannot load such file - 2

    我花了三天的时间用头撞墙,试图弄清楚为什么简单的“rake”不能通过我的规范文件。如果您遇到这种情况:任何文件夹路径中都不要有空格!。严重地。事实上,从现在开始,您命名的任何内容都没有空格。这是我的控制台输出:(在/Users/*****/Desktop/LearningRuby/learn_ruby)$rake/Users/*******/Desktop/LearningRuby/learn_ruby/00_hello/hello_spec.rb:116:in`require':cannotloadsuchfile--hello(LoadError) 最佳

  3. ruby CSV : How can I read a tab-delimited file? - 2

    CSV.open(name,"r").eachdo|row|putsrowend我得到以下错误:CSV::MalformedCSVErrorUnquotedfieldsdonotallow\ror\n文件名是一个.txt制表符分隔文件。我是专门做的。我有一个.csv文件,我转到excel,并将文件保存为.txt制表符分隔的文件。所以它是制表符分隔的。CSV.open不应该能够读取制表符分隔的文件吗? 最佳答案 尝试像这样指定字段分隔符:CSV.open("name","r",{:col_sep=>"\t"}).eachdo|row|

  4. 使用 ACL 调用 upload_file 时出现 Ruby S3 "Access Denied"错误 - 2

    我正在尝试编写一个将文件上传到AWS并公开该文件的Ruby脚本。我做了以下事情:s3=Aws::S3::Resource.new(credentials:Aws::Credentials.new(KEY,SECRET),region:'us-west-2')obj=s3.bucket('stg-db').object('key')obj.upload_file(filename)这似乎工作正常,除了该文件不是公开可用的,而且我无法获得它的公共(public)URL。但是当我登录到S3时,我可以正常查看我的文件。为了使其公开可用,我将最后一行更改为obj.upload_file(file

  5. ruby - Sinatra set cache_control to static files in public folder编译错误 - 2

    我不知道为什么,但是当我设置这个设置时它无法编译设置:static_cache_control,[:public,:max_age=>300]这是我得到的syntaxerror,unexpectedtASSOC,expecting']'(SyntaxError)set:static_cache_control,[:public,:max_age=>300]^我只想将“过期”header设置为css、javaascript和图像文件。谢谢。 最佳答案 我猜您使用的是Ruby1.8.7。Sinatra文档中显示的语法似乎是在Ruby1.

  6. UE4 源码阅读:从引擎启动到Receive Begin Play - 2

    一、引擎主循环UE版本:4.27一、引擎主循环的位置:Launch.cpp:GuardedMain函数二、、GuardedMain函数执行逻辑:1、EnginePreInit:加载大多数模块int32ErrorLevel=EnginePreInit(CmdLine);PreInit模块加载顺序:模块加载过程:(1)注册模块中定义的UObject,同时为每个类构造一个类默认对象(CDO,记录类的默认状态,作为模板用于子类实例创建)(2)调用模块的StartUpModule方法2、FEngineLoop::Init()1、检查Engine的配置文件找出使用了哪一个GameEngine类(UGame

  7. python ffmpeg 使用 pyav 转换 一组图像 到 视频 - 2

    2022/8/4更新支持加入水印水印必须包含透明图像,并且水印图像大小要等于原图像的大小pythonconvert_image_to_video.py-f30-mwatermark.pngim_dirout.mkv2022/6/21更新让命令行参数更加易用新的命令行使用方法pythonconvert_image_to_video.py-f30im_dirout.mkvFFMPEG命令行转换一组JPG图像到视频时,是将这组图像视为MJPG流。我需要转换一组PNG图像到视频,FFMPEG就不认了。pyav内置了ffmpeg库,不需要系统带有ffmpeg工具因此我使用ffmpeg的python包装p

  8. ruby - __FILE__ == $PROGRAM_NAME 在 ruby​​ 中是什么意思? - 2

    这个问题在这里已经有了答案:Whatdoes`if__FILE__==$0`meaninRuby(6个答案)关闭6年前。我在审查Ruby代码时偶然发现了这个语法。代码是:if__FILE__==$PROGRAM_NAME#somecode...end我想__FILE__是一个变量,可以让我获取我所在文件的名称?但是$PROGRAM_NAME简化了什么?另外,为什么这个if语句是必需的,因为程序可以使用或不使用它?

  9. ruby - File.read ("| echo mystring") 是如何工作的? - 2

    我在我正在处理的一些代码中发现了这一点。它旨在解决从磁盘读取key文件的要求。在生产环境中,key文件的内容位于环境变量中。旧代码:key=File.read('path/to/key.pem')新代码:key=File.read('|echo$KEY_VARIABLE')这是如何工作的? 最佳答案 来自IOdocs:Astringstartingwith“|”indicatesasubprocess.Theremainderofthestringfollowingthe“|”isinvokedasaprocesswithappro

  10. ruby - 如何通过 Rubocop 指示打开 & :read as argument to File. - 2

    我有这个代码File.open(file_name,'r'){|file|file.read}但是Rubocop发出警告:Offenses:Style/SymbolProc:Pass&:readasargumenttoopeninsteadofablock.你是怎么做到的? 最佳答案 我刚刚创建了一个名为“t.txt”的文件,其中包含“Hello,World\n”。我们可以按如下方式阅读。File.open('t.txt','r',&:read)#=>"Hello,World\n"顺便说一下,由于第二个参数的默认值是'r',所以这样

随机推荐