├── doc └── mpeg-segmenter-flow.png ├── pkg └── mpeg-segmenter-0.3.0.gem ├── bin ├── mpeg-segmenter └── m3u8-playlist-generator ├── test ├── samples │ └── thesimpsonstrailer │ │ ├── thesimpsons_trailer_iphone_3g_64k.ts │ │ ├── thesimpsons_trailer_iphone_3g_150k.ts │ │ └── thesimpsons_trailer_iphone_3g_240k.ts └── mpeg_segmenter_test.rb ├── presets └── libx264-ios.ffpreset ├── LICENSE ├── lib ├── segmenter.rb └── mpegts │ ├── cli │ ├── mpeg-segmenter.rb │ └── m3u8-playlist-generator.rb │ ├── mpegts_file.rb │ ├── mpegts_constants.rb │ ├── httpls │ ├── httpls_m3u8.rb │ └── httpls_segmenter.rb │ └── mpegts_segment.rb ├── Rakefile └── README.md /doc/mpeg-segmenter-flow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drivo/mpeg-segmenter/HEAD/doc/mpeg-segmenter-flow.png -------------------------------------------------------------------------------- /pkg/mpeg-segmenter-0.3.0.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drivo/mpeg-segmenter/HEAD/pkg/mpeg-segmenter-0.3.0.gem -------------------------------------------------------------------------------- /bin/mpeg-segmenter: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require File.dirname(__FILE__) + '/../lib/segmenter' 4 | 5 | MPEGTS::MpegSegmenterCLI.execute(ARGV); 6 | -------------------------------------------------------------------------------- /bin/m3u8-playlist-generator: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require File.dirname(__FILE__) + '/../lib/segmenter' 4 | 5 | MPEGTS::M3u8PlaylistGeneratorCLI.execute(ARGV); 6 | -------------------------------------------------------------------------------- /test/samples/thesimpsonstrailer/thesimpsons_trailer_iphone_3g_64k.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drivo/mpeg-segmenter/HEAD/test/samples/thesimpsonstrailer/thesimpsons_trailer_iphone_3g_64k.ts -------------------------------------------------------------------------------- /test/samples/thesimpsonstrailer/thesimpsons_trailer_iphone_3g_150k.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drivo/mpeg-segmenter/HEAD/test/samples/thesimpsonstrailer/thesimpsons_trailer_iphone_3g_150k.ts -------------------------------------------------------------------------------- /test/samples/thesimpsonstrailer/thesimpsons_trailer_iphone_3g_240k.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drivo/mpeg-segmenter/HEAD/test/samples/thesimpsonstrailer/thesimpsons_trailer_iphone_3g_240k.ts -------------------------------------------------------------------------------- /presets/libx264-ios.ffpreset: -------------------------------------------------------------------------------- 1 | flags=+loop 2 | cmp=+chroma 3 | subq=5 4 | trellis=1 5 | refs=1 6 | coder=0 7 | me_range=16 8 | sc_threshold=40 9 | i_qfactor=0.71 10 | bt=200k 11 | maxrate=96k 12 | bufsize=96k 13 | rc_eq='blurCplx^(1qComp)' 14 | qcomp=0.1 15 | qmin=10 16 | qmax=51 17 | qdiff=4 18 | level=30 19 | g=30 20 | 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | == mpeg-segmenter 2 | 3 | Copyright (C) by Guido D'Albore (guido@bitstorm.it) 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | -------------------------------------------------------------------------------- /lib/segmenter.rb: -------------------------------------------------------------------------------- 1 | # Copyright (C) by Guido D'Albore (guido@bitstorm.it) 2 | # Licensed under the Apache License, Version 2.0 (the "License"); 3 | # you may not use this file except in compliance with the License. 4 | # You may obtain a copy of the License at 5 | # 6 | # http://www.apache.org/licenses/LICENSE-2.0 7 | # 8 | # Unless required by applicable law or agreed to in writing, software 9 | # distributed under the License is distributed on an "AS IS" BASIS, 10 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | # See the License for the specific language governing permissions and 12 | # limitations under the License. 13 | # 14 | 15 | # Reference Specifications: 16 | # 17 | # * ISO 13818-1 (MPEG-part1) 18 | # * IETF Draft HTTP Live Streaming 19 | 20 | $LOAD_PATH << File.dirname(__FILE__) unless $LOAD_PATH.include?(File.dirname(__FILE__)) 21 | 22 | require 'mpegts/mpegts_constants' 23 | require 'mpegts/mpegts_file' 24 | require 'mpegts/mpegts_segment' 25 | require 'mpegts/httpls/httpls_m3u8' 26 | require 'mpegts/httpls/httpls_segmenter' 27 | require 'mpegts/cli/mpeg-segmenter' 28 | require 'mpegts/cli/m3u8-playlist-generator' 29 | 30 | module MPEGTS 31 | Version = '0.3.0' 32 | Revision = '0' 33 | RevisionDate = "2011-12-30" 34 | 35 | InfoLog = true 36 | WarnLog = true 37 | ErrorLog = true 38 | DebugLog = true 39 | end 40 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'rubygems/package_task' 3 | require 'rake' 4 | require 'rake/clean' 5 | require 'rake/gempackagetask' 6 | require 'rake/rdoctask' 7 | require 'rake/testtask' 8 | 9 | spec = Gem::Specification.new do |s| 10 | s.name = 'mpeg-segmenter' 11 | s.version = '0.3.0' 12 | s.has_rdoc = true 13 | s.extra_rdoc_files = ['README.md', 'LICENSE'] 14 | s.summary = 'MPEG-TS Segmenter for HTTP Live Streaming' 15 | s.description = 'Segmentation of MPEG-TS files into HTTP Live Streaming M3U8.' 16 | s.author = 'Guido D\'Albore' 17 | s.email = 'guido@bitstorm.it' 18 | s.homepage = 'http://www.bitstorm.it' 19 | # s.executables = ['your_executable_here'] 20 | s.files = %w(LICENSE README.md Rakefile) + Dir.glob("{bin,lib,spec}/**/*") 21 | s.require_path = "lib" 22 | s.bindir = "bin" 23 | s.executables = ['mpeg-segmenter', 'm3u8-playlist-generator'] 24 | s.default_executable = 'mpeg-segmenter' 25 | end 26 | 27 | Rake::GemPackageTask.new(spec) do |p| 28 | p.gem_spec = spec 29 | p.need_tar = true 30 | p.need_zip = true 31 | end 32 | 33 | Rake::RDocTask.new do |rdoc| 34 | files =['README', 'LICENSE', 'lib/**/*.rb'] 35 | rdoc.rdoc_files.add(files) 36 | rdoc.main = "README" # page to start on 37 | rdoc.title = "mpeg-segmenter Docs" 38 | rdoc.rdoc_dir = 'doc/rdoc' # rdoc output folder 39 | rdoc.options << '--line-numbers' 40 | end 41 | 42 | Rake::TestTask.new do |t| 43 | t.test_files = FileList['test/**/*.rb'] 44 | end 45 | -------------------------------------------------------------------------------- /lib/mpegts/cli/mpeg-segmenter.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # -*- coding: utf-8 -*- 3 | # 4 | # Copyright (C) by Guido D'Albore (guido@bitstorm.it) 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | 18 | # Reference Specifications: 19 | # 20 | # * ISO 13818-1 (MPEG-part1) 21 | # * IETF Draft HTTP Live Streaming 22 | 23 | module MPEGTS 24 | class MpegSegmenterCLI 25 | def self.execute(argv) 26 | self.new.execute(argv) 27 | end 28 | 29 | def print_usage 30 | puts "Mpeg-segmenter v#{MPEGTS::Version}. Converts MPEG-TS files into HTTP Live Streaming M3U8." 31 | puts "Copyright (C) 2011 by Guido D'Albore (guido@bitstorm.it)" 32 | puts 33 | puts "Usage:" 34 | puts "\t" + File.split($0)[1] + " [segment-base-URL]" 35 | puts 36 | end 37 | 38 | def execute(argv) 39 | if((argv.length < 3) || (argv.length > 4)) 40 | print_usage 41 | exit! 42 | else 43 | input_file = argv[0] # Input .TS filepath 44 | input_file = input_file.sub("//", "/") 45 | input_file = input_file.sub("//", "/") 46 | output_basename = File.basename(input_file, File.extname(input_file)) 47 | duration_in_seconds = argv[1].to_i 48 | output_folder = argv[2] + "/" 49 | base_url = argv[3] # it can be "nil" 50 | base_url = "" if(base_url.nil?) 51 | output_file = output_folder + output_basename + "%d.ts" 52 | end 53 | 54 | begin 55 | segmenter = HTTPLiveStreaming::Segmenter::new(input_file, output_file, output_folder, duration_in_seconds, base_url, output_basename) 56 | segmenter.process 57 | rescue => exception 58 | print_usage 59 | puts exception 60 | exit! 61 | end 62 | end 63 | end 64 | 65 | end 66 | 67 | 68 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | mpeg-segmenter 2 | ===================================== 3 | 4 | Starting from a MPEG-TS file, it segments the stream and prepares the M3U8 in according to 5 | HTTP Live Streaming for iOS. 6 | 7 | `mpeg-segmenter` allows you to generate a fully compliant HTTP Live Streaming video from a standard 8 | video file (AVI, MP4, MKV, etc.) 9 | 10 | Synopsis 11 | -------- 12 | Apple recently defined a new standard for video streaming, currently as draft at 13 | [IETF](http://tools.ietf.org/html/draft-pantos-http-live-streaming-07), called HTTP Live Streaming. 14 | 15 | HTTP Live Streaming is the only protocol allowed for delivering video to iOS devices (i.e. iPhone, iPad and iPod). 16 | It is based on top the HTTP protocol and leverages on MPEG TS format defined in the MPEG part 1 specification (ISO 13818-1). 17 | 18 | General procedure 19 | ----------------- 20 | 21 | Let's suppose we want to deliver a video for iOS devices and we have a file AVI, MP4, MKV or similar 22 | video clip. All we need to do is: 23 | 24 | 1. Convert the video in TS format. I strongly suggest to use `ffmpeg`. 25 | 2. Use `mpeg-segmenter` to prepare the TS segments and M3U8 files. 26 | 3. Copy the output files (`.ts` and `.m3u8` files) generated by `mpeg-segmenter` in your preferred web 27 | server and make them available for download. 28 | 4. Open Safari from your iOS and open the web location of main M3U8 file. 29 | 5. Watch your VIDEO! 30 | 31 | A typical flow involves these different actors which act in the following way: 32 | 33 | 34 | 35 | 36 | Installation 37 | ------------ 38 | 39 | A 1.9.x Ruby platform is required. 40 | 41 | gem install pkg/mpeg-segmenter-0.3.0.gem 42 | 43 | After installation, you can decide to use `mpeg-segmenter' as Ruby library or use its command line interface composed by 44 | the following executables: 45 | 46 | * `mpeg-segmenter`. It segments a TS file in multiple files time aligned. 47 | 48 | * `m3u8-playlist-generator`. It generates the variant playlist (main m3u8 file). 49 | 50 | Encoding 51 | ------------- 52 | We strongly suggest to use last version of [FFmpeg](http://ffmpeg.org/download.html). 53 | 54 | FFmpeg is a powerful encoder let you to generate .TS files from your preferred video format (.AVI, .MP4, etc.). 55 | 56 | TBC 57 | 58 | Usage 59 | ------------- 60 | TBD 61 | 62 | Known Limitations 63 | ------------------ 64 | TBD 65 | 66 | License 67 | ------- 68 | Copyright (C) 2012 by Guido D'Albore. [Licensed under the Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) 69 | -------------------------------------------------------------------------------- /lib/mpegts/cli/m3u8-playlist-generator.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # -*- coding: utf-8 -*- 3 | # 4 | # Copyright (C) by Guido D'Albore (guido@bitstorm.it) 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | 18 | # Reference Specifications: 19 | # 20 | # * ISO 13818-1 (MPEG-part1) 21 | # * IETF Draft HTTP Live Streaming 22 | 23 | module MPEGTS 24 | class M3u8PlaylistGeneratorCLI 25 | def self.execute(argv) 26 | self.new.execute(argv) 27 | end 28 | 29 | def print_usage 30 | puts "M3u8 Variant Playlist Generator v#{MPEGTS::Version}. Prepares M3u8 variant playlists for HTTP Live Streaming." 31 | puts "Copyright (C) 2011 by Guido D'Albore (guido@bitstorm.it)" 32 | puts 33 | puts "Usage:" 34 | puts "\t" + File.split($0)[1] + " [m3u8-base-URL]" 35 | puts 36 | puts "Example (how to generate a variant playlist with 4 different streams):" 37 | puts "\t" + File.split($0)[1] + " variants.m3u8 low.m3u8 250000 med.m3u8 640000 hi.m3u8 1250000 gold.m3u8 2500000 http://hostname/playlist/" 38 | puts 39 | end 40 | 41 | def execute(argv) 42 | if(argv.length < 3) 43 | print_usage 44 | exit! 45 | end 46 | 47 | if((argv.length%2) == 0) 48 | # Gets last element (base URL) 49 | base_url = ARGV.pop 50 | else 51 | base_url = "" 52 | end 53 | 54 | filepath = argv[0] 55 | playlist = HTTPLiveStreaming::M3u8.new(filepath) 56 | 57 | counter = 1 58 | 59 | begin 60 | while(counter < argv.length) 61 | playlist.insertPlaylist(base_url + argv[counter], argv[counter + 1].to_i) 62 | 63 | counter += 2 64 | end 65 | 66 | playlist.close 67 | 68 | puts "Playlist '#{filepath}' created." 69 | rescue => exception 70 | print_usage 71 | puts exception 72 | exit! 73 | end 74 | 75 | end 76 | end 77 | 78 | end 79 | 80 | 81 | -------------------------------------------------------------------------------- /lib/mpegts/mpegts_file.rb: -------------------------------------------------------------------------------- 1 | # Copyright (C) by Guido D'Albore (guido@bitstorm.it) 2 | # Licensed under the Apache License, Version 2.0 (the "License"); 3 | # you may not use this file except in compliance with the License. 4 | # You may obtain a copy of the License at 5 | # 6 | # http://www.apache.org/licenses/LICENSE-2.0 7 | # 8 | # Unless required by applicable law or agreed to in writing, software 9 | # distributed under the License is distributed on an "AS IS" BASIS, 10 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | # See the License for the specific language governing permissions and 12 | # limitations under the License. 13 | # 14 | 15 | # Reference Specifications: 16 | # 17 | # * ISO 13818-1 (MPEG-part1) 18 | # * IETF Draft HTTP Live Streaming 19 | 20 | module MPEGTS 21 | class MpegTsFile 22 | def initialize(tsFilePath) 23 | raise "File \"#{tsFilePath}\" doesn't exist!" if !File.exist?(tsFilePath) 24 | 25 | @filePath = tsFilePath 26 | @fileSize = File.size(filePath) 27 | 28 | raise "File \"#{tsFilePath}\" contains an incorrent size (it must be multiple of 188 bytes)." if self.size%MPEGTS::MPEGTSConstants::TS_PACKET_SIZE != 0 29 | 30 | @segmentCount = File.size(filePath) / MPEGTS::MPEGTSConstants::TS_PACKET_SIZE 31 | 32 | @currentSegment = 0 33 | 34 | # It's better to pre-open the file 35 | # otherwise the time to browse the segment 36 | # increases hugely 37 | #@file = File.open(@filePath, "r:binary") 38 | @file = File.open(@filePath, "rb") 39 | end 40 | 41 | # Getters 42 | def filePath; @filePath; end 43 | def segmentCount; @segmentCount; end 44 | def size; @fileSize ; end 45 | def currentSegment; @currentSegment; end 46 | 47 | def eachSegment(&b) 48 | @currentSegment = 0; 49 | 50 | 0.upto(@segmentCount-1) { 51 | b.call(nextSegment) 52 | } 53 | end 54 | 55 | def nextSegment() 56 | if(@currentSegment == @segmentCount) 57 | # We reached the end of file 58 | return nil 59 | end 60 | 61 | segment = getSegment(@currentSegment) 62 | @currentSegment += 1 63 | 64 | return segment 65 | end 66 | 67 | def getSegment(segmentNumber) 68 | if((segmentNumber < 0) || (segmentNumber >= @segmentCount)) 69 | return nil 70 | end 71 | 72 | @file.seek(MPEGTS::MPEGTSConstants::TS_PACKET_SIZE * segmentNumber, IO::SEEK_SET) 73 | data = @file.read(MPEGTS::MPEGTSConstants::TS_PACKET_SIZE) 74 | 75 | MPEGTSSegment.new(data.bytes.to_a, segmentNumber) 76 | end 77 | 78 | end 79 | end 80 | -------------------------------------------------------------------------------- /test/mpeg_segmenter_test.rb: -------------------------------------------------------------------------------- 1 | # Copyright (C) by Guido D'Albore (guido@bitstorm.it) 2 | # Licensed under the Apache License, Version 2.0 (the "License"); 3 | # you may not use this file except in compliance with the License. 4 | # You may obtain a copy of the License at 5 | # 6 | # http://www.apache.org/licenses/LICENSE-2.0 7 | # 8 | # Unless required by applicable law or agreed to in writing, software 9 | # distributed under the License is distributed on an "AS IS" BASIS, 10 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | # See the License for the specific language governing permissions and 12 | # limitations under the License. 13 | # 14 | 15 | # Reference Specifications: 16 | # 17 | # * ISO 13818-1 (MPEG-part1) 18 | # * IETF Draft HTTP Live Streaming 19 | 20 | $:.unshift File.join(File.dirname(__FILE__),'..','lib') 21 | 22 | require 'test/unit' 23 | require 'segmenter' 24 | require 'fileutils' 25 | 26 | class MpegSegmenterTest < Test::Unit::TestCase 27 | def initialize(test_method_name) 28 | super(test_method_name) 29 | 30 | @sample_dir = File.join(File.dirname(__FILE__),'samples','thesimpsonstrailer') 31 | @output_dir = File.join(@sample_dir,'output') 32 | 33 | FileUtils.rm_rf(@output_dir) if File.exists?(@output_dir) 34 | end 35 | 36 | def setup 37 | Dir.mkdir(@output_dir) if !File.exists?(@output_dir) 38 | end 39 | 40 | def cleanup 41 | # Clears output directory 42 | FileUtils.rm_rf(@output_dir) if File.exists?(@output_dir) 43 | end 44 | 45 | def test1_segmenter_3g_64k 46 | assert_boolean(File.exists?(@output_dir), "Playlist cannot be generated if output directory wasn't created.") 47 | 48 | argv = Array.new 49 | argv[0] = File.join(@sample_dir,'thesimpsons_trailer_iphone_3g_64k.ts') 50 | argv[1] = "10" 51 | argv[2] = @output_dir 52 | 53 | # Executes segmenter 54 | MPEGTS::MpegSegmenterCLI.execute(argv); 55 | 56 | assert_equal(File.size(File.join(@output_dir, 'thesimpsons_trailer_iphone_3g_64k.m3u8')), 766) 57 | assert_equal(File.size(File.join(@output_dir, 'thesimpsons_trailer_iphone_3g_64k1.ts')), 71440) 58 | assert_equal(File.size(File.join(@output_dir, 'thesimpsons_trailer_iphone_3g_64k14.ts')), 58280) 59 | end 60 | 61 | def test2_segmenter_3g_240k 62 | assert_boolean(File.exists?(@output_dir), "Playlist cannot be generated if output directory wasn't created.") 63 | 64 | argv = Array.new 65 | argv[0] = File.join(@sample_dir,'thesimpsons_trailer_iphone_3g_240k.ts') 66 | argv[1] = "10" 67 | argv[2] = @output_dir 68 | 69 | # Executes segmenter 70 | MPEGTS::MpegSegmenterCLI.execute(argv); 71 | 72 | assert_equal(File.size(File.join(@output_dir, 'thesimpsons_trailer_iphone_3g_240k.m3u8')), 780) 73 | assert_equal(File.size(File.join(@output_dir, 'thesimpsons_trailer_iphone_3g_240k1.ts')), 190444) 74 | assert_equal(File.size(File.join(@output_dir, 'thesimpsons_trailer_iphone_3g_240k14.ts')), 187624) 75 | 76 | end 77 | 78 | def test3_m3u8_playlist_generator 79 | assert_boolean(File.exists?(@output_dir), "Playlist cannot be generated if output directory wasn't created.") 80 | 81 | argv = Array.new 82 | argv[0] = File.join(@output_dir,'thesimpsons_trailer_variants.m3u8') 83 | 84 | argv[1] = 'thesimpsons_trailer_iphone_3g_64k.m3u8' 85 | argv[2] = "64000" 86 | 87 | argv[3] = 'thesimpsons_trailer_iphone_3g_240k.m3u8' 88 | argv[4] = "240000" 89 | 90 | # Executes playlist generator 91 | MPEGTS::M3u8PlaylistGeneratorCLI.execute(argv); 92 | end 93 | 94 | end 95 | -------------------------------------------------------------------------------- /lib/mpegts/mpegts_constants.rb: -------------------------------------------------------------------------------- 1 | # Copyright (C) by Guido D'Albore (guido@bitstorm.it) 2 | # Licensed under the Apache License, Version 2.0 (the "License"); 3 | # you may not use this file except in compliance with the License. 4 | # You may obtain a copy of the License at 5 | # 6 | # http://www.apache.org/licenses/LICENSE-2.0 7 | # 8 | # Unless required by applicable law or agreed to in writing, software 9 | # distributed under the License is distributed on an "AS IS" BASIS, 10 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | # See the License for the specific language governing permissions and 12 | # limitations under the License. 13 | # 14 | 15 | # Reference Specifications: 16 | # 17 | # * ISO 13818-1 (MPEG-part1) 18 | # * IETF Draft HTTP Live Streaming 19 | 20 | module MPEGTS 21 | module MPEGTSConstants 22 | TS_PACKET_SIZE = 188 23 | TS_PACKET_SYNC_BYTE = 0x47 24 | TS_PACKET_HEADER_OFFSET = 0 25 | TS_PACKET_HEADER_SIZE = 4 26 | TS_PACKET_PES_PREFIX = 0x000001 27 | TS_PACKET_ADAPTATION_FIELD_LENGTH_OFFSET = 4 28 | 29 | TS_PACKET_ADAPTATION_FIELD_RESERVED = 0x00 30 | TS_PACKET_ADAPTATION_FIELD_PAYLOAD_ONLY = 0x01 31 | TS_PACKET_ADAPTATION_FIELD_ONLY = 0x02 32 | TS_PACKET_ADAPTATION_FIELD_WITH_PAYLOAD = 0x03 33 | 34 | PES_PACKET_STREAM_ID_OFFSET = 3 35 | PES_PACKET_LENGTH_OFFSET = 4 36 | PES_PACKET_STREAM_INFO_OFFSET = 6 37 | PES_PACKET_PTS_DST_OFFSET = 9 38 | 39 | # Stream Identifiers (stream_id field in the PES packet): 40 | # ----------------------------------- 41 | # 10111100 1 program_stream_map 42 | # 10111101 2 private_stream_1 43 | # 10111110 padding_stream 44 | # 10111111 3 private_stream_2 45 | # 110x xxxx ISO/IEC 13818-3 or ISO/IEC 11172-3 or ISO/IEC 13818-7 or ISO/IEC 14496-3 audio stream number x xxxx 46 | # 1110 xxxx ITU-T Rec. H.262 | ISO/IEC 13818-2 or ISO/IEC 11172-2 or ISO/IEC 14496-2 video stream number xxxx 47 | # 1111 0000 3 ECM_stream 48 | # 1111 0001 3 EMM_stream 49 | # 1111 0010 5 ITU-T Rec. H.222.0 | ISO/IEC 13818-1 Annex A or ISO/IEC 13818-6_DSMCC_stream 50 | # 1111 0011 2 ISO/IEC_13522_stream 51 | # 1111 0100 6 ITU-T Rec. H.222.1 type A 52 | # 1111 0101 6 ITU-T Rec. H.222.1 type B 53 | # 1111 0110 6 ITU-T Rec. H.222.1 type C 54 | # 1111 0111 6 ITU-T Rec. H.222.1 type D 55 | # 1111 1000 6 ITU-T Rec. H.222.1 type E 56 | # 1111 1001 7 ancillary_stream 57 | # 1111 1010 ISO/IEC14496-1_SL-packetized_stream 58 | # 1111 1011 ISO/IEC14496-1_FlexMux_stream 59 | # 1111 1100 ... 1111 1110 reserved data stream 60 | # 1111 1111 4 program_stream_directory 61 | 62 | 63 | PES_PACKET_AUDIO_STREAM_ID_MASK = 0b11100000 64 | PES_PACKET_AUDIO_STREAM_NUMBER_MASK = 0b00011111 65 | PES_PACKET_VIDEO_STREAM_ID_MASK = 0b11110000 66 | PES_PACKET_VIDEO_STREAM_NUMBER_MASK = 0b00001111 67 | 68 | PES_PACKET_AUDIO_STREAM_ID = 0b11000000 69 | PES_PACKET_VIDEO_STREAM_ID = 0b11100000 70 | 71 | ADAPTATION_STRING_MAP = { 72 | TS_PACKET_ADAPTATION_FIELD_RESERVED => "TS_PACKET_ADAPTATION_FIELD_RESERVED", 73 | TS_PACKET_ADAPTATION_FIELD_PAYLOAD_ONLY => "TS_PACKET_ADAPTATION_FIELD_PAYLOAD_ONLY", 74 | TS_PACKET_ADAPTATION_FIELD_ONLY => "TS_PACKET_ADAPTATION_FIELD_ONLY", 75 | TS_PACKET_ADAPTATION_FIELD_WITH_PAYLOAD => "TS_PACKET_ADAPTATION_FIELD_WITH_PAYLOAD" 76 | } 77 | 78 | PES_PACKET_PTS_ONLY_VALUE = 0b00000010 79 | PES_PACKET_PTS_AND_DTS_VALUE = 0b00000011 80 | 81 | # In according to the specification the timer is set to 90 kHz 82 | TIMER_IN_HZ = 90000.0; 83 | end 84 | 85 | end -------------------------------------------------------------------------------- /lib/mpegts/httpls/httpls_m3u8.rb: -------------------------------------------------------------------------------- 1 | # Copyright (C) by Guido D'Albore (guido@bitstorm.it) 2 | # Licensed under the Apache License, Version 2.0 (the "License"); 3 | # you may not use this file except in compliance with the License. 4 | # You may obtain a copy of the License at 5 | # 6 | # http://www.apache.org/licenses/LICENSE-2.0 7 | # 8 | # Unless required by applicable law or agreed to in writing, software 9 | # distributed under the License is distributed on an "AS IS" BASIS, 10 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | # See the License for the specific language governing permissions and 12 | # limitations under the License. 13 | # 14 | 15 | # Reference Specifications: 16 | # 17 | # * ISO 13818-1 (MPEG-part1) 18 | # * IETF Draft HTTP Live Streaming 19 | 20 | module HTTPLiveStreaming 21 | class M3u8 22 | def initialize(filepath, target_duration = nil, base_url = nil, media_sequence = nil) 23 | @file = File.open(filepath, "w") 24 | @file.puts("#EXTM3U") 25 | @file.puts("#EXT-X-TARGETDURATION:%d" % target_duration) if(!target_duration.nil?) 26 | @file.puts("#EXT-X-MEDIA-SEQUENCE:%d" % media_sequence) if(!media_sequence.nil?) 27 | @file.flush 28 | 29 | @duration = target_duration 30 | 31 | @url_prefix = base_url 32 | @url_prefix = "segment_" if(base_url.nil?) 33 | @counter = 1 34 | end 35 | 36 | def insert(resourceUrl, duration = @duration, description = "") 37 | @file.puts(sprintf("#EXTINF:%d, %s", duration, description)) 38 | @file.puts(resourceUrl) 39 | end 40 | 41 | def insertMedia(duration = nil) 42 | duration = @duration if(duration.nil?) 43 | 44 | insert(@url_prefix + @counter.to_s + ".ts", duration) 45 | @counter += 1 46 | end 47 | 48 | def insertPlaylist(url, bandwidth, program_id = 1, resolution_width = nil, resolution_height = nil) 49 | if(resolution_width.nil? || resolution_height.nil?) 50 | @file.puts(sprintf("#EXT-X-STREAM-INF:PROGRAM-ID=%d, BANDWIDTH=%d", program_id, bandwidth)) 51 | else 52 | @file.puts(sprintf("#EXT-X-STREAM-INF:PROGRAM-ID=%d, BANDWIDTH=%d, RESOLUTION=%dx%d", program_id, bandwidth, resolution_width, resolution_height)) 53 | end 54 | 55 | @file.puts(url) 56 | end 57 | 58 | def close 59 | @file.puts("#EXT-X-ENDLIST") 60 | @file.close 61 | end 62 | end 63 | end 64 | 65 | __END__ 66 | 67 | ** Usage examples ** 68 | # 69 | #playlist = M3u8.new("/test-low.m3u8", 15, "fragment_low_") 70 | #playlist.insertMedia 71 | #playlist.insertMedia 72 | #playlist.insertMedia 73 | #playlist.close 74 | # 75 | #playlist = M3u8.new("/test-hi.m3u8", 15, "fragment_hi_") 76 | #playlist.insertMedia 77 | #playlist.insertMedia 78 | #playlist.insertMedia 79 | #playlist.close 80 | # 81 | #playlist = M3u8.new("/test_variants.m3u8") 82 | #playlist.insertPlaylist("test-low.m3u8", 250000, 1, 400, 224) 83 | #playlist.insertPlaylist("test-hi.m3u8", 1240000, 1, 640, 360) 84 | #playlist.close 85 | 86 | ** Example M3U8 with bandwidth adaptation ** 87 | --- 88 | ##EXTM3U 89 | #EXT-X-STREAM-INF:PROGRAM-ID=1, BANDWIDTH=240000 90 | iphone_3g/stream.m3u8 91 | #EXT-X-STREAM-INF:PROGRAM-ID=1, BANDWIDTH=640000 92 | iphone_wifi/stream.m3u8 93 | #EXT-X-STREAM-INF:PROGRAM-ID=1, BANDWIDTH=1240000 94 | ipad_wifi/stream.m3u8 95 | --- 96 | 97 | ** Example M3U8 with single segmented media ** 98 | --- 99 | #EXTM3U 100 | #EXT-X-TARGETDURATION:10 101 | #EXT-X-MEDIA-SEQUENCE:0 102 | #EXTINF:10, no desc 103 | fileSequence1.ts 104 | #EXTINF:10, no desc 105 | fileSequence2.ts 106 | #EXTINF:10, no desc 107 | fileSequence3.ts 108 | #EXTINF:10, no desc 109 | fileSequence4.ts 110 | #EXTINF:10, no desc 111 | fileSequence5.ts 112 | #EXTINF:10, no desc 113 | fileSequence6.ts 114 | #EXTINF:10, no desc 115 | fileSequence7.ts 116 | #EXTINF:10, no desc 117 | fileSequence8.ts 118 | #EXTINF:10, no desc 119 | fileSequence9.ts 120 | #EXTINF:10, no desc 121 | fileSequence10.ts 122 | #EXT-X-ENDLIST 123 | --- -------------------------------------------------------------------------------- /lib/mpegts/httpls/httpls_segmenter.rb: -------------------------------------------------------------------------------- 1 | # Copyright (C) by Guido D'Albore (guido@bitstorm.it) 2 | # Licensed under the Apache License, Version 2.0 (the "License"); 3 | # you may not use this file except in compliance with the License. 4 | # You may obtain a copy of the License at 5 | # 6 | # http://www.apache.org/licenses/LICENSE-2.0 7 | # 8 | # Unless required by applicable law or agreed to in writing, software 9 | # distributed under the License is distributed on an "AS IS" BASIS, 10 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | # See the License for the specific language governing permissions and 12 | # limitations under the License. 13 | # 14 | 15 | # Reference Specifications: 16 | # 17 | # * ISO 13818-1 (MPEG-part1) 18 | # * IETF Draft HTTP Live Streaming 19 | 20 | module HTTPLiveStreaming 21 | class Segmenter 22 | def initialize(input_file, output_file, output_path, segment_duration = 10, base_url = nil, resource_name = "stream") 23 | if(!File.directory?(output_path)) 24 | raise "Output directory path #{output_path} is not valid!" 25 | end 26 | 27 | if(!File.exist?(input_file)) 28 | raise "Input file #{input_file} doesn't exist!" 29 | end 30 | 31 | if(!segment_duration.instance_of?(Fixnum)) 32 | raise "Invalid duration value (#{segment_duration}). It must be an integer value." 33 | end 34 | 35 | if(!(segment_duration > 1)) 36 | raise "Invalid duration value (#{segment_duration}). It must greater than 1 second." 37 | end 38 | 39 | begin 40 | @tsFile = MPEGTS::MpegTsFile.new(input_file) 41 | @m3u8file = HTTPLiveStreaming::M3u8.new(output_path + resource_name + ".m3u8", segment_duration, base_url + resource_name) 42 | rescue => exception 43 | raise exception 44 | end 45 | 46 | puts "Input transport streaming file: \"" + @tsFile.filePath + "\"" 47 | puts "Transport streaming size: " + @tsFile.size.to_s 48 | puts "Number of transport segments: " + @tsFile.segmentCount.to_s 49 | 50 | @durationInSeconds = segment_duration; 51 | @inputFile = input_file; 52 | @destinationFile = output_file; 53 | end 54 | 55 | def process 56 | duration_per_segment = @durationInSeconds #seconds 57 | file_count = 1; 58 | last_segment_copied = 0 59 | last_association_table = temp_last_association_table = 0 60 | #last_program_table = 0 61 | segment_count = 0 62 | program_pids = Array.new 63 | absolute_time = 0 64 | 65 | #time1 = Time.now.to_i 66 | reference_time = nil 67 | @tsFile.eachSegment() { |i| 68 | # if((@tsFile.currentSegment%1000) == 0) 69 | # puts "%0.0f%%" % (segment_count.to_f/@tsFile.segmentCount.to_f * 100.0) 70 | # puts "Current segment: " + @tsFile.currentSegment.to_s 71 | # end 72 | 73 | if((segment_count-1) == temp_last_association_table) 74 | # Check if this association table is adiacent to program table 75 | if(program_pids.include?(i.pid)) 76 | # puts "Sequence: " + (i.sequence + 1).to_s 77 | # puts "Program map PID found: mark position" 78 | # mark position for block copying 79 | last_association_table = temp_last_association_table 80 | # puts "-----------------" 81 | end 82 | end 83 | 84 | if(i.pid == 0) 85 | temp_last_association_table = segment_count 86 | program_pids = i.program_PIDs 87 | # puts "PID: Program Association Table" 88 | # puts "Sequence: " + (i.sequence + 1).to_s 89 | # puts "Program PIDS: " + i.program_PIDs.to_s 90 | # puts "-----------------" 91 | end 92 | 93 | # if(i.pid == 1) 94 | # puts "PID: Conditional Access Table" 95 | # puts "Sequence: " + (i.sequence + 1).to_s 96 | # puts "-----------------" 97 | # end 98 | # if(i.pid == 2) 99 | # puts "PID: Transport Stream Description Table" 100 | # puts "Sequence: " + (i.sequence + 1).to_s 101 | # puts "-----------------" 102 | # end 103 | # if(i.pid == 0x1FFF) 104 | # puts "PID: Null Packet" 105 | # puts "Sequence: " + (i.sequence + 1).to_s 106 | # puts "-----------------" 107 | # end 108 | # if((i.pid >= 3) && (i.pid <= 0xF)) 109 | # puts "PID: RESERVED Packet" 110 | # puts "Sequence: " + (i.sequence + 1).to_s 111 | # end 112 | 113 | # Is the Presentation Time Stamp (PTS) defined within the TS packet? 114 | if(i.pts_defined) 115 | 116 | # puts "PID: " + i.pid.to_s 117 | # puts "Sequence: " + (i.sequence + 1).to_s 118 | # puts "Continuity counter: " + i.continuity_counter.to_s 119 | # puts "Adaptation Field Control: " + i.adaptation_field_control.to_s + " (" + ADAPTATION_STRING_MAP[i.adaptation_field_control] + ")" 120 | # puts "PES/PSI Start Indicator: " + i.payload_unit_start_indicator.to_s 121 | # puts "Adaptation Field Size: " + i.adaptationFieldSize.to_s 122 | # puts "PES packet found: " + i.pesPacketFound.to_s 123 | # puts "Video stream: " + i.is_video_stream.to_s 124 | # puts "Audio stream: " + i.is_audio_stream.to_s 125 | # puts "Stream number: " + i.stream_number.to_s 126 | 127 | if(i.pts_defined) 128 | if(reference_time.nil?) 129 | # The first time we get a PTS, we need to setup the reference_time 130 | reference_time = i.pts2seconds 131 | end 132 | 133 | absolute_time = i.pts2seconds - reference_time 134 | #puts "PTS value: " + i.pts.to_s 135 | #puts "PTS relative value (in seconds): " + "%0.4f" % i.pts2seconds.to_s 136 | #puts "PTS absolute value (in seconds): " + "%0.4f" % (absolute_time).to_s 137 | #puts "PTS type: " + (i.pts).class.to_s 138 | 139 | if(absolute_time >= duration_per_segment) 140 | reference_time = i.pts2seconds 141 | @m3u8file.insertMedia 142 | copy_segments_to_file( @inputFile, # Input .TS file 143 | last_segment_copied, # From segment 144 | last_association_table, # To segment 145 | @destinationFile % file_count # Output .TS segment file 146 | ) 147 | last_segment_copied = last_association_table 148 | file_count += 1 149 | end 150 | end 151 | # puts "-----------------" 152 | end 153 | segment_count += 1; 154 | #puts "%02x" % i.data[0] 155 | } 156 | 157 | if(segment_count > last_segment_copied) 158 | 159 | if(absolute_time.round > 0) 160 | # Last segment is greater than 0.5 seconds 161 | @m3u8file.insertMedia(absolute_time.round) 162 | 163 | copy_segments_to_file( @inputFile, # Input .TS file 164 | last_segment_copied, # From segment 165 | segment_count, # To segment 166 | @destinationFile % file_count # Output .TS segment file 167 | ) 168 | else 169 | # Last segment is next to 0 seconds 170 | # We only need to merge it to the previous segment 171 | append_segments_to_file( @inputFile, # Input .TS file 172 | last_segment_copied, # From segment 173 | segment_count, # To segment 174 | @destinationFile % (file_count-1) # Output .TS segment file 175 | ) 176 | end 177 | end 178 | 179 | #time2 = Time.now.to_i; 180 | #puts "Time elapsed: " + (time2-time1).to_s + " seconds" 181 | puts "Segmetation completed. Created #{file_count} segments of #{@durationInSeconds} seconds each." 182 | #segment = @tsFile.nextSegment 183 | #puts segment.data.class 184 | #puts "%02x" % segment.data[0] 185 | 186 | @m3u8file.close 187 | 188 | end 189 | 190 | def copy_segments_to_file(source, from, to, destination) 191 | print "Creating file '" + destination + "'..." 192 | # puts "Writing to file '" + destination + "'..." 193 | # puts "From segment: " + from.to_s 194 | # puts "To segment: " + to.to_s 195 | # puts "Data start: " + (from * TS_PACKET_SIZE).to_s 196 | # puts "Data end: " + ((from * TS_PACKET_SIZE) + ((to-from) * TS_PACKET_SIZE) - 1).to_s 197 | # puts "Data size: " + ((to-from) * TS_PACKET_SIZE).to_s 198 | # puts "-----------------------------" 199 | data = IO.read(source, (to-from) * MPEGTS::MPEGTSConstants::TS_PACKET_SIZE, from * MPEGTS::MPEGTSConstants::TS_PACKET_SIZE) 200 | 201 | # destination_file = File.open(destination, "w:binary") 202 | destination_file = File.open(destination, "wb") 203 | destination_file.write(data) 204 | destination_file.close 205 | 206 | print "done.\n" 207 | end 208 | 209 | def append_segments_to_file(source, from, to, destination) 210 | print "Creating file '" + destination + "'..." 211 | # puts "Writing to file '" + destination + "'..." 212 | # puts "From segment: " + from.to_s 213 | # puts "To segment: " + to.to_s 214 | # puts "Data start: " + (from * TS_PACKET_SIZE).to_s 215 | # puts "Data end: " + ((from * TS_PACKET_SIZE) + ((to-from) * TS_PACKET_SIZE) - 1).to_s 216 | # puts "Data size: " + ((to-from) * TS_PACKET_SIZE).to_s 217 | # puts "-----------------------------" 218 | data = IO.read(source, (to-from) * TS_PACKET_SIZE, from * TS_PACKET_SIZE) 219 | 220 | # destination_file = File.open(destination, "w:binary") 221 | destination_file = File.open(destination, "wb+") 222 | destination_file.seek(0, IO::SEEK_END) # Go to the end of file 223 | destination_file.write(data) 224 | destination_file.close 225 | 226 | print "done.\n" 227 | end 228 | end 229 | end -------------------------------------------------------------------------------- /lib/mpegts/mpegts_segment.rb: -------------------------------------------------------------------------------- 1 | # Copyright (C) by Guido D'Albore (guido@bitstorm.it) 2 | # Licensed under the Apache License, Version 2.0 (the "License"); 3 | # you may not use this file except in compliance with the License. 4 | # You may obtain a copy of the License at 5 | # 6 | # http://www.apache.org/licenses/LICENSE-2.0 7 | # 8 | # Unless required by applicable law or agreed to in writing, software 9 | # distributed under the License is distributed on an "AS IS" BASIS, 10 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | # See the License for the specific language governing permissions and 12 | # limitations under the License. 13 | # 14 | 15 | # Reference Specifications: 16 | # 17 | # * ISO 13818-1 (MPEG-part1) 18 | # * IETF Draft HTTP Live Streaming 19 | 20 | module MPEGTS 21 | class MPEGTSSegment 22 | include MPEGTSConstants 23 | 24 | attr_reader :pid 25 | attr_reader :sequence 26 | attr_reader :continuity_counter 27 | attr_reader :adaptation_field_control 28 | attr_reader :payload_unit_start_indicator 29 | attr_reader :is_video_stream 30 | attr_reader :is_audio_stream 31 | attr_reader :stream_number 32 | attr_reader :pts 33 | attr_reader :pts_defined 34 | attr_reader :program_map_PIDs 35 | 36 | def data; @data; end 37 | 38 | def program_PIDs; @program_map_PIDs; end 39 | 40 | def initialize(segment, sequence) 41 | if(segment[0] != TS_PACKET_SYNC_BYTE) then 42 | puts "Segment '#{sec}' not valid: sync byte 0x47 not found." if ErrorLog 43 | raise "Segment '#{sec}' not valid: sync byte 0x47 not found." 44 | end 45 | 46 | @data = segment 47 | @sequence = sequence 48 | 49 | @stream_id = nil 50 | @stream_number = nil 51 | @pes_packet_length = 0 52 | @is_video_stream = false 53 | @is_audio_stream = false 54 | @pts_defined = false 55 | 56 | # Parse the Header 57 | self.parseHeader 58 | 59 | if(self.pesPacketFound) 60 | self.parsePesPacket 61 | end 62 | 63 | if(self.programAssociationSectionFound) 64 | self.parseProgramAssociationSection 65 | end 66 | end 67 | 68 | 69 | def programAssociationSectionFound 70 | @pid == 0 71 | end 72 | 73 | # Parse the Program Association Section in according to 74 | # ISO 13818-1 specification: 75 | # 76 | # ----------------------------------- 77 | # HEADER (3 bytes) 78 | # 1st byte (MSB -> LSB): 79 | # table_id 8 bits 80 | # 81 | # 2nd/3rd bytes: 82 | # section_syntax_indicator 1 83 | # '0' 1 84 | # reserved 2 85 | # section_length 12 86 | # ----------------------------------- 87 | # 4th/5th bytes (MSB -> LSB): 88 | # transport_stream_id 16 89 | # 90 | # 6th byte: 91 | # reserved 2 92 | # version_number 5 93 | # current_next_indicator 1 94 | # 95 | # 7th byte: 96 | # section_number 8 97 | # 98 | # 8th byte: 99 | # last_section_number 8 imsbf 100 | # 101 | # 9th (for each program, 4 bytes): 102 | # program_number 16 bits 103 | # reserved 3 bits 104 | # network_pid or program_map_PID 13 bits 105 | # 106 | # Last 4 bytes 107 | # CRC32 108 | def parseProgramAssociationSection 109 | offset = self.pasSectionOffset 110 | #puts "PES Section offset: " + offset.to_s if InfoLog 111 | 112 | #First byte of Program Association Section 113 | @table_id = @data[offset + 0] 114 | 115 | #Second byte 116 | info = @data[offset + 1] 117 | @section_syntax_indicator = (info & 0b10000000) != 0 118 | @section_length = (info & 0b00001111) << 8 119 | 120 | #Third byte 121 | @section_length = @section_length | @data[offset + 2] 122 | @number_of_programs = (@section_length - 9) / 4 123 | # puts "PES Section length: " + @section_length.to_s 124 | # puts "Number of programs: " + @number_of_programs.to_s 125 | 126 | # Fourth/Fifth bytes 127 | @transport_stream = (@data[offset + 3] << 8) | @data[offset + 4] 128 | 129 | # Sixth byte 130 | info = @data[offset + 5] 131 | @version_number = (info & 0b00111110) >> 1 132 | @current_next_indicator = (info & 0b00000001) 133 | 134 | # Seventh byte 135 | @section_number = [offset + 6] 136 | 137 | # Eighthth byte 138 | @last_section_number = [offset + 7] 139 | 140 | @program_map_PIDs = Array.new 141 | @network_pid = nil 142 | 0.upto(@number_of_programs-1) { 143 | |i| 144 | base_offset = offset + 8 + i*4; 145 | program_number = (@data[base_offset] << 8) | @data[base_offset + 1] 146 | 147 | # First 3 bits are reverved 148 | pid = ((@data[base_offset + 2] << 8) | @data[base_offset + 3]) & 0b0001111111111111 149 | 150 | if(program_number == 0) 151 | # network_pid 152 | @network_pid = pid 153 | else 154 | @program_map_PIDs << pid 155 | end 156 | } 157 | end 158 | 159 | # The timer in the MPEG-TS is set to 90kHz 160 | def pts2seconds 161 | return pts/TIMER_IN_HZ 162 | end 163 | 164 | # Parse the Header in according to the following table: 165 | # 166 | # Transport Packet Header (4 bytes) 167 | # ----------------------------------- 168 | # 1st byte (MSB -> LSB): 169 | # sync_byte => 8 bits 170 | # 171 | # 2nd/3rd bytes: 172 | # transport_error_indicator => 1 bits 173 | # payload_unit_start_indicator => 1 bits 174 | # transport_priority => 1 bits 175 | # PID => 13 bits 176 | # 177 | # 4th byte: 178 | # transport_scrambling_control => 2 bits 179 | # adaptation_field_control => 2 bits 180 | # continuity_counter => 4 bits 181 | def parseHeader 182 | @sync_byte = @data[0] 183 | @transport_error_indicator = (@data[1] & 0b10000000) != 0 184 | @payload_unit_start_indicator = (@data[1] & 0b01000000) != 0 185 | @transport_priority = (@data[1] & 0b00100000) != 0 186 | @pid = ((@data[1] & 0b00011111) << 8) | @data[2] 187 | @transport_scrambling_control = (@data[3] & 0b11000000) >> 6 188 | @adaptation_field_control = (@data[3] & 0b00110000) >> 4 189 | @continuity_counter = (@data[3] & 0b00001111) 190 | end 191 | 192 | def parsePesPacket 193 | offset = self.pesPacketOffset 194 | 195 | @stream_id = @data[offset + PES_PACKET_STREAM_ID_OFFSET] 196 | @pes_packet_length = (@data[offset + PES_PACKET_LENGTH_OFFSET] << 8) | (@data[offset + PES_PACKET_LENGTH_OFFSET + 1]) 197 | @is_video_stream = (@stream_id & PES_PACKET_VIDEO_STREAM_ID_MASK) == PES_PACKET_VIDEO_STREAM_ID 198 | @is_audio_stream = (@stream_id & PES_PACKET_AUDIO_STREAM_ID_MASK) == PES_PACKET_AUDIO_STREAM_ID 199 | 200 | 201 | if(@is_video_stream) 202 | @stream_number = (@stream_id & PES_PACKET_VIDEO_STREAM_NUMBER_MASK) 203 | end 204 | 205 | if(@is_audio_stream) 206 | @stream_number = (@stream_id & PES_PACKET_AUDIO_STREAM_NUMBER_MASK) 207 | end 208 | 209 | if(@is_video_stream || @is_audio_stream) 210 | =begin 211 | 1st byte (MSB -> LSB): 212 | '10' => 2 bits 213 | PES_scrambling_control => 2 bits 214 | PES_priority => 1 bit 215 | data_alignment_indicator => 1 bit 216 | copyright => 1 bit 217 | original_or_copy => 1 bit 218 | 219 | 2nd byte: 220 | PTS_DTS_flags => 2 bits 221 | ESCR_flag => 1 bit 222 | ES_rate_flag => 1 bit 223 | DSM_trick_mode_flag => 1 bit 224 | additional_copy_info_flag => 1 bit 225 | PES_CRC_flag => 1 bit 226 | PES_extension_flag => 1 bit 227 | 228 | 3rd byte: 229 | PES_header_data_length => 8 bits 230 | =end 231 | 232 | # gets first byte 233 | info = @data[offset + PES_PACKET_STREAM_INFO_OFFSET + 0] 234 | @pes_scrambling_control = (info & 0b00110000) >> 4 235 | @pes_priority = (info & 0b00001000) != 0 236 | @data_alignment_indicator = (info & 0b00000100) != 0 237 | @copyright = (info & 0b00000010) != 0 238 | @original_or_copy = info & 0b00000001 239 | 240 | # gets second byte 241 | info = @data[offset + PES_PACKET_STREAM_INFO_OFFSET + 1] 242 | @pts_dts_flags = (info & 0b11000000) >> 6 243 | @escr_flag = (info & 0b00100000) != 0 244 | @es_rate_flag = (info & 0b00010000) != 0 245 | @dsm_trick_mode_flag = (info & 0b00001000) != 0 246 | @additiona_copy_info_flag = (info & 0b00000100) != 0 247 | @pes_crc_flag = (info & 0b00000010) != 0 248 | @pes_extension_flag = (info & 0b00000001) != 0 249 | 250 | # gets third byte 251 | @pes_header_length = @data[offset + PES_PACKET_STREAM_INFO_OFFSET + 2] 252 | 253 | if( (@pts_dts_flags == PES_PACKET_PTS_ONLY_VALUE) || 254 | (@pts_dts_flags == PES_PACKET_PTS_AND_DTS_VALUE)) 255 | =begin 256 | PTS (presentation time stamp, 90KHz clock) structure: 257 | 258 | 1st byte (MSB -> LSB): 259 | '0010' => 4 bits 260 | PTS [32..30] => 3 bits 261 | marker_bit => 1 bit 262 | 263 | 2nd / 3rd bytes: 264 | PTS [29..15] => 15 bits 265 | marker_bit => 1 bit 266 | 267 | 4th/5th bytes: 268 | PTS [14..0] => 15 bits 269 | marker_bit => 1 bits 270 | =end 271 | # first byte 272 | info = @data[offset + PES_PACKET_PTS_DST_OFFSET + 0] 273 | @pts = (info & 0b00001110) << 29 274 | 275 | # second byte (8 bit of data) 276 | info = @data[offset + PES_PACKET_PTS_DST_OFFSET + 1] 277 | @pts = @pts | (info << 22) 278 | 279 | # third byte (7 bit of data) 280 | info = @data[offset + PES_PACKET_PTS_DST_OFFSET + 2] 281 | @pts = @pts | ((info & 0b11111110) << 14) 282 | 283 | # fourth byte (8 bit of data) 284 | info = @data[offset + PES_PACKET_PTS_DST_OFFSET + 3] 285 | @pts = @pts | (info << 7) 286 | 287 | # fifth byte (7 bit of data)) 288 | info = @data[offset + PES_PACKET_PTS_DST_OFFSET + 4] 289 | @pts = @pts | (info >> 1) 290 | 291 | @pts_defined = true 292 | #puts "PTS class type: " + @pts.class.to_s 293 | end 294 | 295 | # if(@pts_dts_flags == PES_PACKET_PTS_AND_DTS_VALUE) 296 | # # PTS and DTS are declared 297 | # end 298 | end 299 | end 300 | 301 | def pesPacketOffset 302 | # offset 0: packet_start_code_prefix => 24 bits 303 | # offset 3: stream_id => 8 bits 304 | # offset 4: PES_packet_length => 16 bits 305 | 306 | @stream_id = @data[3] 307 | return TS_PACKET_HEADER_SIZE + self.adaptationFieldSize; 308 | end 309 | 310 | def pasSectionOffset 311 | if(@payload_unit_start_indicator) 312 | return TS_PACKET_HEADER_SIZE + self.adaptationFieldSize + 1; 313 | end 314 | 315 | return TS_PACKET_HEADER_SIZE + self.adaptationFieldSize 316 | end 317 | 318 | def pesPacketFound 319 | if( ((@adaptation_field_control == TS_PACKET_ADAPTATION_FIELD_PAYLOAD_ONLY) || 320 | (@adaptation_field_control == TS_PACKET_ADAPTATION_FIELD_WITH_PAYLOAD)) && 321 | @payload_unit_start_indicator) 322 | 323 | offset = self.pesPacketOffset 324 | 325 | byte1 = @data[offset + 0] << 16 326 | byte2 = @data[offset + 1] << 8 327 | byte3 = @data[offset + 2] 328 | 329 | pesprefix = (byte1 | byte2 | byte3) 330 | 331 | #puts "Prefix: " + pesprefix.to_s 332 | return pesprefix == TS_PACKET_PES_PREFIX 333 | end 334 | 335 | return false 336 | end 337 | 338 | def adaptationFieldLegth 339 | if( (@adaptation_field_control == TS_PACKET_ADAPTATION_FIELD_ONLY) || 340 | (@adaptation_field_control == TS_PACKET_ADAPTATION_FIELD_WITH_PAYLOAD) ) 341 | # adaptation field present, the length is defined on the first byte after 342 | # the packet header (4 bytes) 343 | 344 | return @data[TS_PACKET_ADAPTATION_FIELD_LENGTH_OFFSET] 345 | end 346 | 347 | return 0 348 | end 349 | 350 | def adaptationFieldSize 351 | length = self.adaptationFieldLegth 352 | 353 | if(length != 0) 354 | # +1 is the size of "length" field 355 | return length + 1 356 | end 357 | 358 | return 0 359 | end 360 | 361 | 362 | 363 | end 364 | end --------------------------------------------------------------------------------