├── .rspec ├── Rakefile ├── .travis.yml ├── lib ├── rstreamor │ ├── version.rb │ ├── response.rb │ ├── file.rb │ ├── request.rb │ └── stream.rb └── rstreamor.rb ├── spec ├── spec_helper.rb ├── rstreamor_spec.rb └── rstreamor │ └── file_spec.rb ├── Gemfile ├── .gitignore ├── bin ├── setup └── console ├── rstreamor.gemspec └── README.md /.rspec: -------------------------------------------------------------------------------- 1 | --format documentation 2 | --color 3 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | rvm: 3 | - 2.2.0 4 | -------------------------------------------------------------------------------- /lib/rstreamor/version.rb: -------------------------------------------------------------------------------- 1 | module Rstreamor 2 | VERSION = "0.2.6" 3 | end 4 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | $LOAD_PATH.unshift File.expand_path('../../lib', __FILE__) 2 | require 'rstreamor' 3 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Specify your gem's dependencies in rstreamor.gemspec 4 | gemspec 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /Gemfile.lock 4 | /_yardoc/ 5 | /coverage/ 6 | /doc/ 7 | /pkg/ 8 | /spec/reports/ 9 | /tmp/ 10 | .idea -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | 5 | bundle install 6 | 7 | # Do any other automated setup that you need to do here 8 | -------------------------------------------------------------------------------- /spec/rstreamor_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Rstreamor do 4 | it 'has a version number' do 5 | expect(Rstreamor::VERSION).not_to be nil 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /lib/rstreamor.rb: -------------------------------------------------------------------------------- 1 | require 'rstreamor/version' 2 | require 'rstreamor/file' 3 | require 'rstreamor/request' 4 | require 'rstreamor/response' 5 | require 'rstreamor/stream' 6 | module Rstreamor 7 | include Rstreamor::Stream 8 | end 9 | -------------------------------------------------------------------------------- /spec/rstreamor/file_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Rstreamor::File do 4 | 5 | subject { Rstreamor::File.new nil } 6 | 7 | it { is_expected.to respond_to :data } 8 | it { is_expected.to respond_to :content_type } 9 | it { is_expected.to respond_to :size } 10 | end 11 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require "bundler/setup" 4 | require "rstreamor" 5 | 6 | # You can add fixtures and/or initialization code here to make experimenting 7 | # with your gem easier. You can also use a different console, if you like. 8 | 9 | # (If you use this, don't forget to add pry to your Gemfile!) 10 | # require "pry" 11 | # Pry.start 12 | 13 | require "irb" 14 | IRB.start 15 | -------------------------------------------------------------------------------- /lib/rstreamor/response.rb: -------------------------------------------------------------------------------- 1 | module Rstreamor 2 | class Response 3 | attr_reader :request 4 | 5 | def initialize(request) 6 | @request = request 7 | end 8 | 9 | def response_code 10 | request.range_header? ? 206 : 200 11 | end 12 | 13 | def content_length 14 | if request.range_header? 15 | (request.upper_bound - request.lower_bound + 1).to_s 16 | else 17 | request.file_size 18 | end 19 | end 20 | 21 | def content_range 22 | "bytes #{request.lower_bound}-#{request.upper_bound}/#{request.file_size}" 23 | end 24 | 25 | def accept_ranges 26 | 'bytes' 27 | end 28 | 29 | def cache_control 30 | 'no-cache' 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /lib/rstreamor/file.rb: -------------------------------------------------------------------------------- 1 | module Rstreamor 2 | class File 3 | attr_reader :file 4 | 5 | def initialize(file) 6 | @file = file 7 | end 8 | 9 | def data 10 | @data ||= if file.respond_to? :data 11 | file.data 12 | else 13 | file.read 14 | end 15 | end 16 | 17 | def content_type 18 | file.content_type 19 | end 20 | 21 | def size 22 | # Because of encoding, the string size might not match the stream size, 23 | # and that may lead to files being truncated when served (i.e. this happens 24 | # often for MP3 files, were the last few seconds of files longer than a minute 25 | # gets trimmed). 26 | @size ||= data.bytesize 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /lib/rstreamor/request.rb: -------------------------------------------------------------------------------- 1 | module Rstreamor 2 | class Request 3 | attr_reader :request, :file 4 | 5 | def initialize(request, file) 6 | @request = request 7 | @file = file 8 | end 9 | 10 | def ranges 11 | request.headers['HTTP_RANGE'].gsub('bytes=', '').split('-') if request.headers['HTTP_RANGE'] 12 | end 13 | 14 | def upper_bound 15 | ranges[1] ? ranges[1].to_i : (file.size - 1) 16 | end 17 | 18 | def lower_bound 19 | ranges[0] ? ranges[0].to_i : 0 20 | end 21 | 22 | def range_header? 23 | request.headers['HTTP_RANGE'].present? 24 | end 25 | 26 | def file_content_type 27 | file.content_type 28 | end 29 | 30 | def slice_file 31 | if request.headers['HTTP_RANGE'].present? 32 | file.data.byteslice(lower_bound..upper_bound) 33 | else 34 | file.data 35 | end 36 | end 37 | 38 | def file_size 39 | file.size 40 | end 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /lib/rstreamor/stream.rb: -------------------------------------------------------------------------------- 1 | module Rstreamor 2 | module Stream 3 | def stream(file, options = {}) 4 | request_builder = Rstreamor::Request.new(request, Rstreamor::File.new(file)) 5 | response_builder = Rstreamor::Response.new(request_builder) 6 | set_response_header(request_builder, response_builder) 7 | stream_file(request_builder, response_builder, options) 8 | end 9 | 10 | private 11 | 12 | def stream_file(request_builder, response_builder, options) 13 | content = request_builder.slice_file 14 | 15 | send_data(content, { 16 | type: request_builder.file_content_type, 17 | disposition: 'inline', 18 | status: response_builder.response_code 19 | }.merge(options)) 20 | end 21 | 22 | def set_response_header(request_builder, response_builder) 23 | response.headers['Content-Type'] = request_builder.file_content_type 24 | response.headers['Content-Length'] = response_builder.content_length 25 | if request_builder.range_header? 26 | response.headers['Accept-Ranges'] = 'bytes' 27 | response.headers['Cache-Control'] = 'no-cache' 28 | response.headers['Content-Range'] = response_builder.content_range 29 | end 30 | end 31 | end 32 | end -------------------------------------------------------------------------------- /rstreamor.gemspec: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | lib = File.expand_path('../lib', __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require 'rstreamor/version' 5 | 6 | Gem::Specification.new do |spec| 7 | spec.name = 'rstreamor' 8 | spec.version = Rstreamor::VERSION 9 | spec.authors = ['Erwin Schens'] 10 | spec.email = ['erwinschens@uni-koblenz.de'] 11 | 12 | spec.summary = %q{Stream files using HTTP range requests.} 13 | spec.description = %q{ 14 | Rstreamor gives you the power to stream your files using the HTTP range requests defined in the HTTP/1.1. 15 | Range requests are an optional feature of HTTP, 16 | designed so that recipients not implementing this feature 17 | (or not supporting it for the target resource) can respond as if 18 | it is a normal GET request without impacting interoperability. 19 | Partial responses are indicated by a distinct status code to not be mistaken for full responses by caches that might not implement the feature. 20 | } 21 | spec.homepage = 'https://github.com/ndea/rstreamor' 22 | spec.license = 'MIT' 23 | 24 | spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) } 25 | spec.bindir = 'exe' 26 | spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } 27 | spec.require_paths = ['lib'] 28 | 29 | spec.add_development_dependency 'bundler', '~> 1.8' 30 | spec.add_development_dependency 'rake', '~> 10.0' 31 | spec.add_development_dependency 'rspec', '~> 3.3.0' 32 | end 33 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Rstreamor 2 | Stream your data using HTTP/1.1 range requests and partial responses. 3 | 4 | # Get Rstreamor 5 | ###### Directly from GitHub 6 | ```ruby 7 | gem 'rstreamor', git: 'https://github.com/ndea/rstreamor.git', branch: 'master' 8 | ``` 9 | ###### Rubygems 10 | ```ruby 11 | gem 'rstreamor', '~> 0.2.6' 12 | ``` 13 | # Install 14 | ```ruby 15 | bundle install 16 | ``` 17 | # Usage 18 | In combination with Carrierwave 19 | ```ruby 20 | class Profile < ActiveRecord::Base 21 | mount_uploader :image_file, ProfileImageUploader 22 | end 23 | ``` 24 | ```ruby 25 | class VideosController < ApplicationController 26 | include Rstreamor 27 | def show 28 | stream @resource.image_file 29 | end 30 | end 31 | ``` 32 | Rstreamor takes care of the rest. 33 | If you dont use Carrierwave as a file make sure your file method has the following methods defined: 34 | - #data 35 | - #content_type 36 | 37 | Optionally, you could send some params to the `stream` method like the following example: 38 | ```ruby 39 | class VideosController < ApplicationController 40 | include Rstreamor 41 | def show 42 | stream @resource.image_file, { x_sendfile: true, stream: true } 43 | end 44 | end 45 | ``` 46 | 47 | Please note that if you don't specify any range request headers Rstreamor will return the whole file from byte 0 to EOF with status code *200* 48 | 49 | # What is a range request? 50 | Byte serving is the process of sending only a portion of an HTTP/1.1 message from a server to a client. Byte serving begins when an HTTP server advertises its willingness to serve partial requests using the Accept-Ranges response header. A client then requests a specific part of a file from the server using the Range request header. If the range is valid, the server sends it to the client with a 206 Partial Content status code and a Content-Range header listing the range sent. Clients which request byte-serving might do so in cases in which a large file has been only partially delivered and a limited portion of the file is needed in a particular range. Byte Serving is therefore a method of bandwidth optimization. [Wikipedia](https://en.wikipedia.org/wiki/Byte_serving) 51 | 52 | ### HTML5 video / audio streaming 53 | Consider you have a large video or audio file on your server which needs to be served partially to your client. You don't want to send the whole file in one response (unless you want to download the file). Instead the client needs only partial content which he can view and request other partial content if needed. Rstreamor provides this byte serving mechanism defined in HTTP/1.1. 54 | 55 | ###### Example 56 | Example of the request - response flow 57 | Consider simple HTML5 video streaming. 58 | ```html 59 | 63 | ``` 64 | The first request fired by the client contains the following header 65 | ```bash 66 | Range:bytes=0- 67 | ``` 68 | and requests the whole file from the server starting from 0 till end. The server then responds with the following headers 69 | ```bash 70 | Accept-Ranges:bytes 71 | Cache-Control:no-cache 72 | Content-Disposition:inline 73 | Content-Length:6642801 74 | Content-Range:bytes 0-6642800/6642801 75 | Content-Transfer-Encoding:binary 76 | Content-Type:application/mp4 77 | ``` 78 | Now let's skip through our video and seek for a certain scene - the client now sends the following request header 79 | ```bash 80 | Range:bytes=3303604- 81 | ``` 82 | And the server responds with the following response headers 83 | ```bash 84 | Accept-Ranges:bytes 85 | Cache-Control:no-cache 86 | Content-Disposition:inline 87 | Content-Length:3339197 88 | Content-Range:bytes 3303604-6642800/6642801 89 | Content-Transfer-Encoding:binary 90 | Content-Type:application/mp4 91 | ``` 92 | # Contributing 93 | 94 | 1. Fork it ( https://github.com/ndea/regressor/fork ) 95 | 2. Create your feature branch (`git checkout -b my-new-feature`) 96 | 3. Commit your changes (`git commit -am 'Add some feature'`) 97 | 4. Push to the branch (`git push origin my-new-feature`) 98 | 5. Create a new Pull Request 99 | 100 | # Contribution topics wanted 101 | 102 | - Specs 103 | - Documentation 104 | - Bugfixes 105 | - Codestyle 106 | - Anything that improves this gem 107 | --------------------------------------------------------------------------------