├── .gitignore ├── .rspec ├── .travis.yml ├── Dockerfile ├── Gemfile ├── Gemfile.lock ├── LICENSE ├── README.md ├── Rakefile ├── bin ├── console └── setup ├── dash_timeline_validator.gemspec ├── exe └── dash_timeline_validator ├── imgs └── example.png ├── lib ├── dash_timeline_validator.rb └── dash_timeline_validator │ ├── adaptation_set.rb │ ├── cli.rb │ ├── file.rb │ ├── log.rb │ ├── period.rb │ ├── report.rb │ ├── representation.rb │ ├── segment.rb │ ├── validator.rb │ └── version.rb └── spec ├── dash_timeline_validator_spec.rb └── spec_helper.rb /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /_yardoc/ 4 | /coverage/ 5 | /doc/ 6 | /pkg/ 7 | /spec/reports/ 8 | /tmp/ 9 | /*.gem 10 | /data 11 | # rspec failure tracking 12 | .rspec_status 13 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --format documentation 2 | --color 3 | --require spec_helper 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | --- 2 | sudo: false 3 | language: ruby 4 | cache: bundler 5 | rvm: 6 | - 2.5.0 7 | before_install: gem install bundler -v 2.0.1 8 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ruby:2.6.2-alpine3.9 2 | 3 | RUN apk add --no-cache ffmpeg mediainfo git build-base 4 | RUN gem install -v '0.1.2' dash_timeline_validator 5 | ENTRYPOINT ["/usr/local/bundle/bin/dash_timeline_validator"] 6 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | # Specify your gem's dependencies in dash_timeline_validator.gemspec 4 | gemspec 5 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: . 3 | specs: 4 | dash_timeline_validator (0.1.2) 5 | awesome_print (~> 1.8.0) 6 | iso8601 (~> 0.10.1) 7 | ox (~> 2.9.0) 8 | 9 | GEM 10 | remote: https://rubygems.org/ 11 | specs: 12 | awesome_print (1.8.0) 13 | diff-lcs (1.3) 14 | iso8601 (0.10.1) 15 | ox (2.9.4) 16 | rake (13.0.1) 17 | rspec (3.8.0) 18 | rspec-core (~> 3.8.0) 19 | rspec-expectations (~> 3.8.0) 20 | rspec-mocks (~> 3.8.0) 21 | rspec-core (3.8.0) 22 | rspec-support (~> 3.8.0) 23 | rspec-expectations (3.8.2) 24 | diff-lcs (>= 1.2.0, < 2.0) 25 | rspec-support (~> 3.8.0) 26 | rspec-mocks (3.8.0) 27 | diff-lcs (>= 1.2.0, < 2.0) 28 | rspec-support (~> 3.8.0) 29 | rspec-support (3.8.0) 30 | 31 | PLATFORMS 32 | ruby 33 | 34 | DEPENDENCIES 35 | bundler (~> 2.0) 36 | dash_timeline_validator! 37 | rake (~> 13.0) 38 | rspec (~> 3.0) 39 | 40 | BUNDLED WITH 41 | 2.0.1 42 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2019, Globo.com 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | 3. Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Dash Timeline Validator 2 | 3 | This tool allows you to validate your [MPEG Dash](https://en.wikipedia.org/wiki/Dynamic_Adaptive_Streaming_over_HTTP) manifest to find if there are errors related to the presentation timeline model. 4 | 5 | ![Example](imgs/example.png) 6 | 7 | ## Docker Usage 8 | 9 | ``` 10 | docker run --rm -it anafrombr/dash_timeline_validator https://storage.googleapis.com/shaka-live-assets/player-source.mpd --verify_segments_duration false 11 | ``` 12 | 13 | ## Installation 14 | 15 | Add this line to your application's Gemfile: 16 | 17 | ```ruby 18 | gem 'dash_timeline_validator' 19 | ``` 20 | 21 | And then execute: 22 | 23 | $ bundle 24 | 25 | Or install it yourself as: 26 | 27 | $ gem install dash_timeline_validator 28 | 29 | ## Usage 30 | 31 | Run this program passing the manifest path. It can be either a URI or local path. 32 | 33 | ``` 34 | dash_timeline_validator https://storage.googleapis.com/shaka-live-assets/player-source.mpd 35 | ``` 36 | 37 | Running the program without any parameters or using `h` will show usage instruction along with optional parameters. 38 | 39 | ### Options 40 | 41 | - `acceptable_drift *(default 2)*` - the minimum duration drift acceptable between the sequential segments 42 | - `presentation_delay *(default 10)*` - the delay in seconds of the live edge 43 | - `buffered_segments *(default 2)*` - the number of segments buffered by the player to generate the live edge 44 | - `verify_segments_duration *(default false)*` - check the duration of every segment when setted to `true` (warn: this will download every segment of the manifest) 45 | - `analyzer_folder *(default "data/[HASH]")*` - folder used to download the files 46 | - `analyzer_manifest_path *(default "#{analyzer_folder}/manifest.mpd")*` - manifest path 47 | 48 | Example: 49 | 50 | ``` 51 | dash_timeline_validator https://storage.googleapis.com/shaka-live-assets/player-source.mpd --acceptable_drift 2 52 | ``` 53 | 54 | ### What does it validates? 55 | 56 | 1. The advised timeline segments - basically, if the ` d=>` [is summing up right](https://github.com/globocom/dash_timeline_validator/blob/master/lib/dash_timeline_validator/segment.rb#L24-L30). Our audio segments were drifting (due to a round we made) and this made the exoplayer behave as if it were buffering while most of the other players didn't show any problem at all. It optionally [download and check whether the advised duration](https://github.com/globocom/dash_timeline_validator/blob/master/lib/dash_timeline_validator/segment.rb#L45-L64) equals to the one being served. 57 | 2. The advised timeline - if the [possible live edge is contained within the advised timeline](https://github.com/globocom/dash_timeline_validator/blob/master/lib/dash_timeline_validator/representation.rb#L34-L55) (to use client wall clock, ast, mbt, player buffer to see what should be one possible live edge) 58 | 59 | ## Development 60 | 61 | After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment. 62 | 63 | To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org). 64 | 65 | ## Contributing 66 | 67 | Bug reports and pull requests are welcome on GitHub at https://github.com/globocom/dash_timeline_validator. 68 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | require "rspec/core/rake_task" 3 | 4 | RSpec::Core::RakeTask.new(:spec) 5 | 6 | task :default => :spec 7 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require "bundler/setup" 4 | require "dash_timeline_validator" 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(__FILE__) 15 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | set -vx 5 | 6 | bundle install 7 | 8 | # Do any other automated setup that you need to do here 9 | -------------------------------------------------------------------------------- /dash_timeline_validator.gemspec: -------------------------------------------------------------------------------- 1 | 2 | lib = File.expand_path("../lib", __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require "dash_timeline_validator/version" 5 | 6 | Gem::Specification.new do |spec| 7 | spec.name = "dash_timeline_validator" 8 | spec.version = DashTimelineValidator::VERSION 9 | spec.authors = ["Ana Carolina Castro"] 10 | spec.email = ["ana.castro@corp.globo.com"] 11 | 12 | spec.summary = %q{MPEG-Dash Timeline Validator} 13 | spec.description = %q{MPEG-Dash Timeline Validator.} 14 | spec.homepage = "https://github.com/globocom/dash_timeline_validator" 15 | 16 | # Prevent pushing this gem to RubyGems.org. To allow pushes either set the 'allowed_push_host' 17 | # to allow pushing to a single host or delete this section to allow pushing to any host. 18 | if spec.respond_to?(:metadata) 19 | spec.metadata["homepage_uri"] = spec.homepage 20 | spec.metadata["source_code_uri"] = "https://github.com/globocom/dash_timeline_validator" 21 | spec.metadata["changelog_uri"] = "https://github.com/globocom/dash_timeline_validator" 22 | else 23 | raise "RubyGems 2.0 or newer is required to protect against " \ 24 | "public gem pushes." 25 | end 26 | 27 | # Specify which files should be added to the gem when it is released. 28 | # The `git ls-files -z` loads the files in the RubyGem that have been added into git. 29 | spec.files = Dir.chdir(File.expand_path("..", __FILE__)) do 30 | `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) } 31 | end 32 | spec.bindir = "exe" 33 | spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } 34 | spec.require_paths = ["lib"] 35 | 36 | spec.add_dependency "iso8601", "~> 0.10.1" 37 | spec.add_dependency "ox", "~> 2.9.0" 38 | spec.add_dependency "awesome_print", "~> 1.8.0" 39 | 40 | spec.add_development_dependency "bundler", "~> 2.0" 41 | spec.add_development_dependency "rake", "~> 13.0" 42 | spec.add_development_dependency "rspec", "~> 3.0" 43 | end 44 | -------------------------------------------------------------------------------- /exe/dash_timeline_validator: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require "optparse" 4 | require "dash_timeline_validator" 5 | 6 | ARGV.push('-h') if ARGV.empty? 7 | 8 | options = {} 9 | OptionParser.new do |opts| 10 | opts.banner = <<-BANNER 11 | DASH Timeline Validator #{DashTimelineValidator::VERSION} 12 | 13 | Usage: dash_timeline_validator [options] 14 | 15 | manifest - either a URI or local path 16 | 17 | Options: 18 | BANNER 19 | 20 | opts.on("--acceptable_drift=ACCEPTABLE_DRIFT", "Minimum duration drift acceptable between the sequential segments", Integer) do |v| 21 | options["acceptable_drift"] = v 22 | end 23 | opts.on("--presentation_delay=PRESENTATION_DELAY", "Delay in seconds of the live edge", Integer) do |v| 24 | options["presentation_delay"] = v 25 | end 26 | opts.on("--buffered_segments=BUFFERED_SEGMENTS", "Number of segments buffered by the player to generate the live edge", Integer) do |v| 27 | options["buffered_segments"] = v 28 | end 29 | opts.on("--verify_segments_duration=VERIFY_SEGMENTS_DURATION", "Check the duration of every segment when setted to true") do |v| 30 | options["verify_segments_duration"] = v 31 | end 32 | opts.on("--analyzer_folder=ANALYZER_FOLDER", "Folder used to download the files") do |v| 33 | options["analyzer_folder"] = v 34 | end 35 | opts.on("--analyzer_manifest_path=ANALYZER_MANIFEST_PATH", "Manifest path") do |v| 36 | options["analyzer_manifest_path"] = v 37 | end 38 | end.parse! 39 | 40 | DashTimelineValidator::CLI.main(ARGV[0], options) 41 | -------------------------------------------------------------------------------- /imgs/example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/globocom/dash_timeline_validator/cabe90c1f404c22ce3631cd3a885ecc463cc5960/imgs/example.png -------------------------------------------------------------------------------- /lib/dash_timeline_validator.rb: -------------------------------------------------------------------------------- 1 | require "dash_timeline_validator/adaptation_set" 2 | require "dash_timeline_validator/cli" 3 | require "dash_timeline_validator/file" 4 | require "dash_timeline_validator/log" 5 | require "dash_timeline_validator/period" 6 | require "dash_timeline_validator/report" 7 | require "dash_timeline_validator/representation" 8 | require "dash_timeline_validator/segment" 9 | require "dash_timeline_validator/validator" 10 | require "dash_timeline_validator/version" 11 | 12 | 13 | module DashTimelineValidator 14 | class Error < StandardError; end 15 | end 16 | -------------------------------------------------------------------------------- /lib/dash_timeline_validator/adaptation_set.rb: -------------------------------------------------------------------------------- 1 | module DashTimelineValidator 2 | class AdaptationSet 3 | def self.process(context, adaptation_set, index) 4 | as_result = {} 5 | as_result["name"] = "AdaptationSet-#{index}" 6 | DashTimelineValidator::Report.fill_report(as_result, adaptation_set, "mimeType") 7 | DashTimelineValidator::Report.fill_report(as_result, adaptation_set, "contentType") 8 | all_representations = adaptation_set.nodes.select { |n| n.name == "Representation" } 9 | 10 | as_result["representations"] = all_representations.each_with_index.map do |representation, i| 11 | DashTimelineValidator::Representation.process({root: context[:root], previous: as_result}, representation, i) 12 | end 13 | 14 | as_result 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /lib/dash_timeline_validator/cli.rb: -------------------------------------------------------------------------------- 1 | require "fileutils" 2 | require "securerandom" 3 | 4 | module DashTimelineValidator 5 | ANALYZER_FOLDER = "data/#{SecureRandom.uuid}".freeze 6 | ANALYZER_MANIFEST_PATH = "#{ANALYZER_FOLDER}/manifest.mpd" 7 | 8 | DEFAULTS = { 9 | "acceptable_drift" => true, 10 | "presentation_delay" => 10, 11 | "buffered_segments" => 2, 12 | "verify_segments_duration" => false, 13 | "analyzer_folder" => ANALYZER_FOLDER, 14 | "analyzer_manifest_path" => ANALYZER_MANIFEST_PATH 15 | } 16 | 17 | def self.set_options(options) 18 | @@options = DEFAULTS.merge!(options) 19 | end 20 | 21 | def self.get_option(name) 22 | @@options[name] 23 | end 24 | 25 | class CLI 26 | def self.error_exit(report) 27 | DashTimelineValidator::Log.info(report) 28 | exit(-1) 29 | end 30 | 31 | def self.main(manifest, options = {}) 32 | DashTimelineValidator.set_options(options) 33 | begin 34 | FileUtils.mkdir_p DashTimelineValidator.get_option("analyzer_folder") 35 | DashTimelineValidator::Log.info("The manifest #{manifest} will be processed at #{DashTimelineValidator.get_option("analyzer_folder")} folder.") 36 | 37 | mpd_content = DashTimelineValidator::DashFile.fetch_file(manifest) 38 | 39 | DashTimelineValidator::Log.info(DashTimelineValidator::Validator.analyze(manifest, mpd_content)) 40 | rescue StandardError => e 41 | DashTimelineValidator::Log.error("There was an error: #{e.inspect}") 42 | DashTimelineValidator::Log.warn("Removing the folder #{DashTimelineValidator.get_option("analyzer_folder")}") 43 | FileUtils.rm_rf DashTimelineValidator.get_option("analyzer_folder") 44 | end 45 | end 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /lib/dash_timeline_validator/file.rb: -------------------------------------------------------------------------------- 1 | require "net/http" 2 | 3 | module DashTimelineValidator 4 | class DashFile 5 | def self.fetch_file(origin, file_path = DashTimelineValidator::ANALYZER_MANIFEST_PATH) 6 | dirname = File.dirname(file_path) 7 | unless File.directory? dirname 8 | FileUtils.mkdir_p(dirname) 9 | end 10 | 11 | if uri? origin 12 | download_and_save(origin, file_path) 13 | else 14 | FileUtils.cp origin, file_path 15 | File.read file_path 16 | end 17 | end 18 | 19 | def self.download_and_save(uri, path) 20 | content = Net::HTTP.get(URI.parse(uri)) 21 | File.write(path, content) 22 | content 23 | end 24 | 25 | def self.uri?(string) 26 | uri = URI.parse(string) 27 | %w( http https ).include?(uri.scheme) 28 | rescue URI::BadURIError 29 | false 30 | rescue URI::InvalidURIError 31 | false 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /lib/dash_timeline_validator/log.rb: -------------------------------------------------------------------------------- 1 | require "awesome_print" 2 | 3 | module DashTimelineValidator 4 | class Log 5 | def self.info(msg) 6 | ap msg 7 | end 8 | 9 | def self.warn(msg) 10 | ap msg 11 | end 12 | 13 | def self.error(msg) 14 | ap msg 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /lib/dash_timeline_validator/period.rb: -------------------------------------------------------------------------------- 1 | module DashTimelineValidator 2 | class Period 3 | def self.process(context, period, index) 4 | period_result = {} 5 | period_result["name"] = "Period-#{index}" 6 | DashTimelineValidator::Report.fill_report(period_result, period, "start", 0, :duration_iso8601_to_i) 7 | 8 | all_adaptation_sets = period.nodes.select { |n| n.name == "AdaptationSet" } 9 | 10 | period_result["adaptation_sets"] = all_adaptation_sets.each_with_index.map do |adaptation_set, i| 11 | DashTimelineValidator::AdaptationSet.process({root: context[:root], previous: period_result}, adaptation_set, i) 12 | end 13 | 14 | period_result 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /lib/dash_timeline_validator/report.rb: -------------------------------------------------------------------------------- 1 | require "iso8601" 2 | 3 | module DashTimelineValidator 4 | class Report 5 | def self.report_info(value) 6 | value 7 | end 8 | 9 | def self.report_warn(value) 10 | "[WARN] #{value}" 11 | end 12 | 13 | def self.report_error(value) 14 | "[ERROR] #{value}" 15 | end 16 | 17 | def self.fill_report(report, mpd_leaf, key_name, default_value = nil, parser_fn = :identity) 18 | if mpd_leaf.respond_to? key_name 19 | report[key_name] = Report.report_info(self.send(parser_fn, mpd_leaf[key_name])) 20 | elsif default_value 21 | report[key_name] = Report.report_info(self.send(parser_fn, default_value)) 22 | end 23 | end 24 | 25 | def self.fill_report_mandatory(report, mpd_leaf, key_name, parser_fn = :identity) 26 | if !mpd_leaf.respond_to? key_name 27 | report[key_name] = report_error("Mandatory #{key_name} is missing") 28 | error_exit(report) 29 | else 30 | report[key_name] = Report.report_info(self.send(parser_fn, mpd_leaf[key_name])) 31 | end 32 | end 33 | 34 | def self.duration_iso8601_to_i(start) 35 | ISO8601::Duration.new(start).to_seconds 36 | end 37 | 38 | def self.time_to_i(value) 39 | Time.parse(value).to_i 40 | end 41 | 42 | def self.identity(value) 43 | value 44 | end 45 | 46 | def self.to_i(value) 47 | value.to_i 48 | end 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /lib/dash_timeline_validator/representation.rb: -------------------------------------------------------------------------------- 1 | module DashTimelineValidator 2 | class Representation 3 | def self.process(context, representation, index) 4 | representation_result = {} 5 | representation_result["name"] = "Representation-#{index}" 6 | 7 | DashTimelineValidator::Report.fill_report(representation_result, representation, "bandwidth") 8 | DashTimelineValidator::Report.fill_report(representation_result, representation, "codecs") 9 | DashTimelineValidator::Report.fill_report(representation_result, representation, "presentationTimeOffset", 0, :duration_iso8601_to_i) 10 | 11 | if representation.respond_to? "SegmentTemplate" 12 | segment_template = representation.SegmentTemplate 13 | representation_result["SegmentTemplate"] = {} 14 | 15 | DashTimelineValidator::Report.fill_report_mandatory(representation_result["SegmentTemplate"], segment_template, "timescale", :to_i) 16 | DashTimelineValidator::Report.fill_report_mandatory(representation_result["SegmentTemplate"], segment_template, "media") 17 | DashTimelineValidator::Report.fill_report(representation_result["SegmentTemplate"], segment_template, "initialization") 18 | DashTimelineValidator::Report.fill_report_mandatory(representation_result["SegmentTemplate"], segment_template, "startNumber") 19 | 20 | if context[:root]["mpd"]["type"].eql? "dynamic" 21 | report_edge_timeline_information = report_edge_timeline_information(context, representation_result, segment_template.SegmentTimeline.nodes) 22 | representation_result["SegmentTemplate"]["timeline"] = report_edge_timeline_information 23 | end 24 | 25 | if segment_template.respond_to? "SegmentTimeline" 26 | DashTimelineValidator::Segment.process({root: context[:root], previous: representation_result["SegmentTemplate"]}, segment_template.SegmentTimeline.nodes) 27 | end 28 | end 29 | 30 | representation_result 31 | end 32 | 33 | def self.report_edge_timeline_information(context, representation_result, ss) 34 | client_wallclock = context[:root]["client_wallclock"] 35 | ast = context[:root]["mpd"]["availabilityStartTime"] 36 | timescale = representation_result["SegmentTemplate"]["timescale"] 37 | max_duration = ss.map { |s| s[:d].to_i }.max / timescale 38 | min_buffer_time = context[:root]["mpd"]["minBufferTime"] 39 | suggested_resentation_delay = context[:root]["mpd"]["suggestedPresentationDelay"] 40 | default_presentation_delay = [DashTimelineValidator.get_option("presentation_delay"), (min_buffer_time * 1.5)].max 41 | timeline_delay = suggested_resentation_delay.nil? ? default_presentation_delay : suggested_resentation_delay 42 | 43 | # suggested streaming edge based on shaka's behavior 44 | streaming_edge = client_wallclock - ast - DashTimelineValidator.get_option("buffered_segments") * max_duration - timeline_delay 45 | 46 | last_segment = ss.last 47 | last_available_time = (last_segment[:t].to_i + (last_segment[:d].to_i * last_segment[:r].to_i)) / timescale 48 | 49 | if streaming_edge > last_available_time 50 | report = DashTimelineValidator::Report.report_warn("Live edge is at #{streaming_edge}s but last segment in timeline starts at #{last_available_time}s") 51 | else 52 | report = DashTimelineValidator::Report.report_info("Live edge is at #{streaming_edge}s and last segment in timeline starts at #{last_available_time}s") 53 | end 54 | 55 | report 56 | end 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /lib/dash_timeline_validator/segment.rb: -------------------------------------------------------------------------------- 1 | module DashTimelineValidator 2 | class Segment 3 | def self.process(context, ss) 4 | mpd_type = context[:root]["mpd"]["type"] 5 | previous = context[:previous] 6 | previous["S"] = [] 7 | 8 | ss.each_with_index do |s, index| 9 | unless s.respond_to? "d" 10 | previous["S"].push(DashTimelineValidator::Report.report_error("Segment (#{index + 1}) doen't have mandatory value for 'd'")) 11 | error_exit(previous) 12 | end 13 | if mpd_type.eql? "dynamic" and !s.respond_to? "t" 14 | previous["S"].push(DashTimelineValidator::Report.report_warn("Segment doen't have a value for 't', it's necessary for MPD with 'dynamic' type")) 15 | end 16 | end 17 | 18 | timeline_segments = ss.map { |s| {d: s.d.to_i, t: s.respond_to?("t") ? s.t.to_i : 0, r: s.respond_to?("r") ? s.r.to_i : 0} } 19 | 20 | current_segment_number = context[:previous]["startNumber"].to_i 21 | 22 | timeline_segments.each_with_index do |current_segment, index| 23 | unless index.zero? 24 | previous_segment = timeline_segments[index - 1] 25 | current_segment_time = current_segment[:t] 26 | expected_segment_time = (previous_segment[:t] + (previous_segment[:d]) * (1 + previous_segment[:r])) 27 | drift = (expected_segment_time - current_segment_time).abs 28 | if drift > DashTimelineValidator.get_option("acceptable_drift") 29 | previous["S"].push(DashTimelineValidator::Report.report_warn("Timeline of was expected to be #{expected_segment_time}, but is #{current_segment_time} (drift = #{drift})")) 30 | end 31 | end 32 | 33 | if DashTimelineValidator::get_option("verify_segments_duration") 34 | (current_segment[:r].to_i + 1).times do |i| 35 | duration_report = check_segment_duration(context, current_segment, current_segment_number, i.zero?) 36 | previous["S"].push(duration_report) if duration_report 37 | current_segment_number += 1 38 | end 39 | end 40 | end 41 | previous.delete("S") if previous["S"].empty? 42 | end 43 | 44 | def self.check_segment_duration(context, current_segment, current_segment_number, download_init = true) 45 | init = context[:previous]["initialization"] 46 | media = context[:previous]["media"] 47 | base_path = context[:root]["base_path"] 48 | init_path = "#{DashTimelineValidator.get_option("analyzer_folder")}/#{init}" 49 | 50 | DashTimelineValidator::DashFile.fetch_file("#{base_path}/#{init}", init_path) if download_init 51 | 52 | segment_file = media.gsub("$Number$", current_segment_number.to_s) 53 | segment_path = "#{DashTimelineValidator.get_option("analyzer_folder")}/#{segment_file}" 54 | full_segment_path = "#{DashTimelineValidator.get_option("analyzer_folder")}/#{segment_file}".gsub(".", "-complete.") 55 | DashTimelineValidator::DashFile.fetch_file("#{base_path}/#{segment_file}", segment_path) 56 | 57 | `cat #{init_path} #{segment_path} > #{full_segment_path}` 58 | duration = `mediainfo --Inform="General;%Duration%" #{full_segment_path}`.to_i 59 | File.delete segment_path 60 | File.delete full_segment_path 61 | mediainfo_duration = duration.to_f / 1000 * context[:previous]["timescale"].to_i 62 | if (mediainfo_duration != current_segment[:d]) 63 | return DashTimelineValidator::Report.report_warn("Mediainfo shows different duration for #{segment_file} compared to the advertised segment timeline item (#{(mediainfo_duration - current_segment[:d]).abs})") 64 | end 65 | end 66 | end 67 | end 68 | -------------------------------------------------------------------------------- /lib/dash_timeline_validator/validator.rb: -------------------------------------------------------------------------------- 1 | require "ox" 2 | 3 | module DashTimelineValidator 4 | class Validator 5 | def self.analyze(manifest, mpd_content) 6 | report = {} 7 | mpd = nil 8 | 9 | if DashTimelineValidator::DashFile.uri? manifest 10 | uri = URI(manifest) 11 | report["base_path"] = DashTimelineValidator::Report.report_info("#{uri.scheme}://#{uri.host}#{uri.path.split("/").reverse.drop(1).reverse.join("/")}") 12 | else 13 | report["base_path"] = DashTimelineValidator::Report.report_info(DashTimelineValidator.get_option("analyzer_folder")) 14 | end 15 | 16 | begin 17 | mpd = Ox.load(mpd_content) 18 | rescue 19 | DashTimelineValidator::Log.error("Error while parsing #{manifest} it might be malformed (a non 2xx response)") 20 | error_exit(report) 21 | end 22 | 23 | report["client_wallclock"] = DashTimelineValidator::Report.report_info(client_wallclock(mpd)) 24 | 25 | report["mpd"] = {} 26 | 27 | DashTimelineValidator::Report.fill_report(report["mpd"], mpd.MPD, "type", "static") 28 | DashTimelineValidator::Report.fill_report_mandatory(report["mpd"], mpd.MPD, "minBufferTime", :duration_iso8601_to_i) 29 | 30 | if report["mpd"]["type"] == "dynamic" 31 | DashTimelineValidator::Report.fill_report_mandatory(report["mpd"], mpd.MPD, "availabilityStartTime", :time_to_i) 32 | if mpd.MPD.respond_to? "suggestedPresentationDelay" 33 | DashTimelineValidator::Report.fill_report(report["mpd"], mpd.MPD, "suggestedPresentationDelay", 0, :duration_iso8601_to_i) 34 | end 35 | else 36 | DashTimelineValidator::Report.fill_report(report["mpd"], mpd.MPD, "availabilityStartTime", Time.now.iso8601(0), :time_to_i) 37 | end 38 | 39 | all_periods = mpd.MPD.nodes.select { |n| n.name == "Period" } 40 | report["mpd"]["periods"] = all_periods.each_with_index.map do |period, index| 41 | DashTimelineValidator::Period.process({root: report, previous: report["mpd"]}, period, index) 42 | end 43 | 44 | report 45 | end 46 | 47 | def self.client_wallclock(mpd) 48 | if mpd.MPD.respond_to? "UTCTiming" 49 | if mpd.MPD.UTCTiming["schemeIdUri"].eql? "urn:mpeg:dash:utc:direct:2014" 50 | raw_time = mpd.MPD.UTCTiming["value"] 51 | elsif mpd.MPD.UTCTiming["schemeIdUri"].eql? "urn:mpeg:dash:utc:http-iso:2014" 52 | raw_time = Net::HTTP.get(URI.parse(mpd.MPD.UTCTiming["value"])) 53 | end 54 | end 55 | 56 | if raw_time.nil? 57 | return DateTime.now.to_time.to_i 58 | end 59 | 60 | DateTime.parse(raw_time).to_time.to_i 61 | end 62 | end 63 | end 64 | -------------------------------------------------------------------------------- /lib/dash_timeline_validator/version.rb: -------------------------------------------------------------------------------- 1 | module DashTimelineValidator 2 | VERSION = "0.1.2" 3 | end 4 | -------------------------------------------------------------------------------- /spec/dash_timeline_validator_spec.rb: -------------------------------------------------------------------------------- 1 | RSpec.describe DashTimelineValidator do 2 | it "has as version number" do 3 | expect(DashTimelineValidator::VERSION).not_to be_empty 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require "bundler/setup" 2 | 3 | RSpec.configure do |config| 4 | # Enable flags like --only-failures and --next-failure 5 | config.example_status_persistence_file_path = ".rspec_status" 6 | 7 | # Disable RSpec exposing methods globally on `Module` and `main` 8 | config.disable_monkey_patching! 9 | 10 | config.expect_with :rspec do |c| 11 | c.syntax = :expect 12 | end 13 | end 14 | --------------------------------------------------------------------------------