All of my client's podcasts start over around the twenty to forty second mark in the iTunes app after the iOS 11 update. iOS 10 player does not display this issue. No other player is known to demonstrate this issue and once the episode restarts it plays to the end without issue. The restart stays at same point in any given episode but I suspect it occurs farther in on longer episodes as if it is maybe at a certain percentage of the duration in.
FF and RW functions including dragging playhead all work.
iTunes podcast app does this restart whether or not the app is allowed to download the episode file.
We have been working unsuccessfully with Apple support for weeks and I thought I would pose this here as well.
Apple support have examined the mp3 files and the RSS feed and see no issues.
<enclosure url="http://example.com/audio/episodes/2018_04_18_1458_some-show.mp3" length="63773956" type="audio/mpeg"/>
<pubDate>Wed, 18 Apr 2018 00:00:00 -0500</pubDate>
<itunes:duration>1:06:25</itunes:duration>
Client lives and dies by Google Analytics stats so we route inbound requests for the MP3s using mod_rewrite in htaccess like so
RewriteCond %{HTTP_REFERER} !^http://(www\.)?example\.com [NC]
RewriteRule ^audio/episodes/([^/\.]+).mp3$ /audio/episodes/google_analytics_mp3.php?mp3=$1&redirected=1 [L,QSA]
And testing has demonstrated that when the routing is removed the issue is not seen in previously unplayed episodes. Episodes which have previously been played continue to exhibit the issue.
google_analytics_mp3.php:
<?php
include_once($_SERVER['DOCUMENT_ROOT']."/classDBI/classDBI.php");
include_once($_SERVER['DOCUMENT_ROOT']."/classDBI/google_analytics_api.php");
$e = new Episode;
if (is_numeric($_REQUEST['mp3'])) {
$id = intval($_REQUEST['mp3']);
list($ep) = $e->retrieve("id = $id");
if ($ep) {
$outcome = ga_send_pageview(basename($ep->audio_link), 'Site streaming: ' . $ep->google_title());
}
} else if ($_REQUEST['redirected'] == 1) {
$fileName = $_GET['mp3'] . '.mp3';
$fileLocation = $_SERVER['DOCUMENT_ROOT'].'/audio/episodes/'.$fileName;
$etag = md5_file($fileName);
$fileRedirect = '/audio/episodes/'.$fileName;
if (file_exists($fileLocation)) {
list($ep) = $e->retrieve("audio_link = 'audio/episodes/$fileName'");
$pageName = 'Direct access';
if ($ep) {
$pageName .= ': ' . $ep->google_title();
}
$size = filesize($fileLocation);
$time = date('r', filemtime($fileLocation));
$fm = @fopen($fileLocation, 'rb');
if (!$fm) {
header ("HTTP/1.1 505 Internal server error");
return;
}
$begin = 0;
$end = $size - 1;
if (isset($_SERVER['HTTP_RANGE'])) {
if (preg_match('/bytes=\h*(\d+)-(\d*)[\D.*]?/i', $_SERVER['HTTP_RANGE'], $matches)) {
$begin = intval($matches[1]);
if (!empty($matches[2])) {
$end = intval($matches[2]);
}
}
}
if (isset($_SERVER['HTTP_RANGE'])) {
header('HTTP/1.1 206 Partial Content');
if ($begin == 0) {
$outcome = ga_send_pageview($fileName, 'Streaming: ' . $pageName);
}
} else{
header('HTTP/1.1 200 OK');
$outcome = ga_send_pageview($fileName, $pageName);
}
header("Content-Transfer-Encoding: binary");
header('Content-Type: audio/mpeg');
header("Etag: $etag");
header('Cache-Control: public, must-revalidate, max-age=0');
header('Pragma: no-cache');
header('Accept-Ranges: bytes');
header('Content-Length:' . (($end - $begin) + 1));
header('Content-Disposition: inline; filename="'.$fileName.'"');
// 2018.01.14 changes
if (isset($_SERVER['HTTP_RANGE'])) {
header("Content-Range: bytes $begin-$end/$size");
}
// END 2018.01.14 changes
readfile($fileLocation);
header("Last-Modified: $time");
$cur = $begin;
fseek($fm, $begin, 0);
while(!feof($fm) && $cur <= $end && (connection_status() == 0)) {
print fread($fm, min(1024 * 16, ($end - $cur) + 1));
$cur += 1024 * 16;
}
exit();
}
}
I feel pretty certain that I something about the way headers are handled is causing the restart issue but I can't find the error.
The following are headers from Chrome Dev panel on a request for an mp3. IP address altered as was name of file:
General
Request URL: http://example.com/audio/episodes/2018_04_18_1458_some-show.mp3
Request Method: GET
Status Code: 200 OK
Remote Address: 205.196.xxx.xxx:80
Referrer Policy: no-referrer-when-downgrade
Response Headers
Accept-Ranges: bytes
Cache-Control: public, must-revalidate, max-age=0
Connection: Keep-Alive
Content-Disposition: inline; filename="2018_04_18_1458_some-show.mp3"
Content-Length: 63773956
Content-Transfer-Encoding: binary
Content-Type: audio/mpeg
Date: Fri, 20 Apr 2018 16:27:57 GMT
Etag: 7fe7b0375cd99ec4d928b1a8885bee81
Expires: Thu, 19 Nov 1981 08:52:00 GMT
Keep-Alive: timeout=2, max=100
Pragma: no-cache
Server: Apache
Request Headers
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8
Accept-Encoding: gzip, deflate
Accept-Language: en-US,en;q=0.9
Cache-Control: no-cache
Connection: keep-alive
Cookie: PHPSESSID=TYDy9IZXJwTCBlmXVmkHn0; _ga=GA1.2.904913110.1515511103; _gid=GA1.2.666994959.1523989189; __unam=739f578-160db803df3-1cf2ee83-70
Host: www.example.com
Pragma: no-cache
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.117 Safari/537.36
In case it helps here is google_analytics_api.php:
<?php
define('GOOGLEACCOUNT', 'UA-xxxxxxxx-1');
define('GOOGLEDOMAIN', $_SERVER['HTTP_HOST']);
function gaParseCookie() {
if (isset($_COOKIE['_ga'])) {
list($version, $domainDepth, $cid1, $cid2) = explode('.', $_COOKIE["_ga"], 4);
$contents = array('version' => $version, 'domainDepth' => $domainDepth, 'cid' => $cid1 . '.' . $cid2);
$cid = $contents['cid'];
} else {
$cid = gaGenerateUUID();
}
return $cid;
}
function gaGenerateUUID() {
return sprintf('%04x%04x-%04x-%04x-%04x-%04x%04x%04x',
mt_rand(0, 0xffff), mt_rand(0, 0xffff),
mt_rand(0, 0xffff),
mt_rand(0, 0x0fff) | 0x4000,
mt_rand(0, 0x3fff) | 0x8000,
mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0xffff)
);
}
function gaSendData($data) {
$getString = 'https://ssl.google-analytics.com/collect';
$getString .= '?payload_data&';
$getString .= http_build_query($data);
$options = array(
CURLOPT_CUSTOMREQUEST =>"GET",
CURLOPT_POST =>false,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_ENCODING => "",
CURLOPT_CONNECTTIMEOUT => 120,
CURLOPT_TIMEOUT => 120,
CURLOPT_MAXREDIRS => 10,
);
$ch = curl_init( $getString );
curl_setopt_array( $ch, $options );
$result = curl_exec( $ch );
$err = curl_errno( $ch );
$errmsg = curl_error( $ch );
$header = curl_getinfo( $ch );
if ($err) {
mail('email@example.com', 'Analytics MP3 Error' . __FILE__, "$err\n\n$errmsg\n\n$header");
}
curl_close( $ch );
return $result;
}
function ga_send_pageview($file, $title) {
$data = array(
'v' => 1,
'tid' => GOOGLEACCOUNT,
'cid' => gaParseCookie(),
't' => 'pageview',
'dh' => GOOGLEDOMAIN,
'dp' => $file,
'dt' => $title
);
if(strlen($_SERVER['HTTP_REFERER'])) {
$data['utmr'] = $_SERVER['HTTP_REFERER'];
}
if (strlen($_SERVER['HTTP_USER_AGENT'])) {
$data['ua'] = $_SERVER['HTTP_USER_AGENT'];
}
gaSendData($data);
}
function ga_send_event($category=null, $action=null, $label=null) {
$data = array(
'v' => 1,
'tid' => GOOGLEACCOUNT,
'cid' => gaParseCookie(),
't' => 'event',
'ec' => $category, //Category (Required)
'ea' => $action, //Action (Required)
'el' => $label
);
gaSendData($data);
}
Any advice most appreciated!
The issue turned out to be that the podcast app seems to have changed what it sends for the end of the HTTP_RANGE on initial requests to 1 like so:
[HTTP_RANGE] => bytes=0-1
This code was parsing the header
if (isset($_SERVER['HTTP_RANGE'])) {
if (preg_match('/bytes=\h*(\d+)-(\d*)[\D.*]?/i', $_SERVER['HTTP_RANGE'], $matches)) {
$begin = intval($matches[1]);
if (!empty($matches[2])) {
$end = intval($matches[2]);
}
}
}
And the resulting outbound Content-Range header would look something like 0-1/59829252
This would result in an immediate second request restarting the podcast.
Fixed by replacing end value of 1 with $size - 1.