Wejn s.r.o.

Solving complicated IT problems is our hobby.

Digging Deeper Into the SMIL Playback Problem With Wowza

A few days ago I wrote about a problem with the way Wowza handles some SMIL request over HLS.

After a rather lengthy e-mail exchange with Richard Lanham I decided to dig deeper into this issue because it seemed quite hard to replicate consistently.

What I found is that the problem only crops up when all of the following holds:

  • your SMIL file has the same name (not including extension) as the referenced video file
  • your video/src in that SMIL is either name or mp4:name
  • your request has the smil:name form

My test results follow, including the script you can use to replicate it all.

First the smil file contents:

smil file video/src tag
test0.smil sample
test1.smil mp4:sample
test2.smil sample.mp4
test3.smil mp4:sample.mp4
sample0.smil sample0
sample1.smil mp4:sample1
sample2.smil sample2.mp4
sample3.smil mp4:sample3.mp4

And now the actual results:

call url smil video src result
test0 sample empty chunklist
test0.smil sample OK
smil:test0 sample OK
smil:test0.smil sample OK
test1 mp4:sample empty chunklist
test1.smil mp4:sample OK
smil:test1 mp4:sample OK
smil:test1.smil mp4:sample OK
test2 sample.mp4 empty chunklist
test2.smil sample.mp4 OK
smil:test2 sample.mp4 OK
smil:test2.smil sample.mp4 OK
test3 mp4:sample.mp4 empty chunklist
test3.smil mp4:sample.mp4 OK
smil:test3 mp4:sample.mp4 OK
smil:test3.smil mp4:sample.mp4 OK
sample0 sample0 OK
sample0.smil sample0 OK
smil:sample0 sample0 Error: looped (wowza bug)
smil:sample0.smil sample0 OK
sample1 mp4:sample1 OK
sample1.smil mp4:sample1 OK
smil:sample1 mp4:sample1 Error: looped (wowza bug)
smil:sample1.smil mp4:sample1 OK
sample2 sample2.mp4 OK
sample2.smil sample2.mp4 OK
smil:sample2 sample2.mp4 OK
smil:sample2.smil sample2.mp4 OK
sample3 mp4:sample3.mp4 OK
sample3.smil mp4:sample3.mp4 OK
smil:sample3 mp4:sample3.mp4 OK
smil:sample3.smil mp4:sample3.mp4 OK

By looped (wowza bug) I mean the originally reported problem where playback stalls.

The reason for the stalling is simple: when HLS player requests chunk list from Wowza, it doesn’t include media.ts entries as it should. Instead it contains link to the chunk list itself (same url!). No wonder the stream can’t be played.

Anyway – the results were generated by this test script I whipped up:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
#!/usr/bin/env ruby

require 'open-uri'

def gen_smil(file, video)
  File.open(file, 'w') do |f|
      f.puts <<-EOF
<smil>
<head></head>
<body>
<switch>
 <video src="#{video}" system-bitrate="500000"
     system-screen-size="240x424" width="424" height="240">
 </video>
</switch>
</body>
</smil>
     EOF
  end
end

def gen_smils(content_dir)
  videos = {
      'sample' => 'test0.smil',
      'mp4:sample' => 'test1.smil',
      'sample.mp4' => 'test2.smil',
      'mp4:sample.mp4' => 'test3.smil',
      #
      'sample0' => 'sample0.smil',
      'mp4:sample1' => 'sample1.smil',
      'sample2.mp4' => 'sample2.smil',
      'mp4:sample3.mp4' => 'sample3.smil',
  }

  out = {}
  puts "smil file;video/src tag" unless $VERBOSE
  for video, name in videos
      gen_smil(File.join(content_dir, name), video)
      if $VERBOSE
          puts "Generating #{name} with video/src: #{video}"
      else
          puts "#{name};#{video}"
      end
      out[name] = video
  end
  out
end

def request_chunklist_for(hls_url)
  puts "Requesting PL: #{hls_url}" if $VERBOSE
  pl = open(hls_url).readlines.reject { |x| x =~ /^#/ }
  pl.map!(&:strip)
  case pl.size
  when 0
      puts "Empty playlist, giving up." if $VERBOSE
  when 1
      puts "Single stream." if $VERBOSE
  else
      puts "Multiple streams, duh?" if $VERBOSE
  end
  for chunklist in pl
      cl_url = hls_url.sub(/playlist\.m3u8$/, chunklist)
      puts "Requesting CL: #{cl_url}" if $VERBOSE
      chunks = open(cl_url).readlines.reject { |x| x =~ /^#/ }
      chunks.map!(&:strip)
      media, rest = chunks.partition do |x|
          x =~ /^media[-_].*\.ts\?wowzasessionid=/
      end
      looped, garbage = rest.partition do |x|
          x =~ /^chunklist.*\.m3u8\?wowzasessionid=/
      end

      if media.empty? && rest.empty?
          puts "empty chunklist"
      elsif !media.empty? && rest.empty?
          puts "OK"
      elsif media.empty? && !looped.empty? && garbage.empty?
          puts "Error: looped (wowza bug)"
      else
          m = media.size
          l = looped.size
          g = garbage.size
          puts "Error: something else: m:#{m}, l:#{l}, g:#{g}"
      end
  end
  nil
end

if __FILE__ == $0
  unless ARGV.size == 2
      STDERR.puts "Usage: #{File.basename($0)} <wms_url> <content_dir>"
      exit 1
  end

  wms = ARGV.shift
  content_dir = ARGV.shift

  unless FileTest.directory?(content_dir)
      STDERR.puts "Content dir '#{content_dir}' doesn't exist"
      exit 1
  end

  unless FileTest.exists?(File.join(content_dir, 'sample.mp4'))
      STDERR.puts "sample.mp4 doesn't exist in content dir"
      exit 1
  end

  for i in (0..3)
      src = File.join(content_dir, 'sample.mp4')
      name = File.join(content_dir, 'sample' + i.to_s + '.mp4')
      puts "Generating #{name} (hardlink to #{src})" if $VERBOSE
      File.link(src, name) rescue nil
  end

  smils = gen_smils(content_dir)

  puts
  puts "call url;smil video src;result" unless $VERBOSE

  ext = ".smil"
  tpl = %w[N N.smil smil:N smil:N.smil]

  for smil, video in smils
      for sn in tpl.map { |x| x.sub(/N/, File.basename(smil, ext)) }
          url = [wms, sn, 'playlist.m3u8'].join('/')
          if $VERBOSE
              puts "Request: #{smil} with #{video}, called as: #{sn}"
              request_chunklist_for(url)
              puts
          else
              print "#{sn};#{video};"
              request_chunklist_for(url)
          end
      end
  end
end

which I called like this:

1
$ ./script.rb "http://wowza.local:1935/vod" "/path/to/Wowza/content"

and got the tables outlined above as result (in CSV format).

Calling the script via ruby -v would produce more verbose output.

See for yourself if you can replicate it.