Angular+Nodejs+S3 app videos doesnt work on iPhones - ios

Works on: Windows PC, Ubuntu PC, MacOS Chrome
Does not work on: iPhone Chrome, iPhone Safari, MacOs Safari
Video just doesn't play there are no errors. Images are working on all platforms.
I am uploading raw videos to S3 with nodejs and multer, tried to use ffmpeg to encode videos to but didnt help.
public upload(file) {
const fileStream = fs.createReadStream(file.path)
let fileKey = `${file.filename}`
const uploadParams = {
Bucket: bucketName,
Body: fileStream,
Key: fileKey,
}
return s3.upload(uploadParams).promise()
}
Nodejs:
public async download(fileKey) {
const downloadParams = {
Key: fileKey,
Bucket: bucketName,
}
try {
const object = await s3.getObject(downloadParams).promise() // This throws error if image not found and catch get it
return {readStream: s3.getObject(downloadParams).createReadStream(), object}
} catch (err) {
throw new NotFoundError()
}
}
router.get('/file', async (req, res) => {
const { key } = req.query
const { readStream, object } = await Container.get(S3Service).download(key)
if (readStream.httpCode) {
res.json({ error: 'error' })
return
}
if(key.split('.')[1] === 'mp4') {
const range = req.headers.range
const bytes = range.replace(/bytes=/, '').split('-')
const start = parseInt(bytes[0], 10)
const total = object.ContentLength
const end = bytes[1] ? parseInt(bytes[1], 10) : total - 1
const chunksize = end - start + 1
res.status(206)
res.set('Content-Type', object.ContentType)
res.set('Content-Length', chunksize)
res.set('Content-Disposition', 'inline')
res.set('Accept-Ranges', 'bytes')
res.set('Accept-Encoding', 'Identity')
res.set('Content-Range', 'bytes ' + start + '-' + end + '/' + total)
res.set('X-Playback-Session-Id', req.header('X-Playback-Session-Id'))
res.set('Connection', 'keep-alive')
res.set('Last-Modified', object.LastModified)
res.set('ETag', object.ETag)
} else {
res.type('png')
}
readStream.pipe(res)
})
Angular:
<video playsinline muted autoplay controls="true" preload="metadata">
<source [src]="message.files[0].fileUrl" type="video/mp4" />
<source [src]="message.files[0].fileUrl" type="video/avi" />
<source [src]="message.files[0].fileUrl" type="video/ogg" />
<source [src]="message.files[0].fileUrl" type="video/webm" />
</video>

dev,
Seems browser capability issue.
Try to run the website in chrome dev tools as iphone and try to do same with ie
Sometime all browser doesn’t support all feature. Majorly on mobile browser, we not have dev option see what is going wrong with dev tools
Regards,
Muhamed

As far as i know to work with videos on apple devices your server must support range request to play video.
Range requests: https://developer.mozilla.org/en-US/docs/Web/HTTP/Range_requests
You can google more details about safari and range requests on stack overflow or google

Related

Audio.play() not working with iOS user agent

I'm making a playlist app in react, where each Track component in the playlist has and <audio> element:
const AudioPlayer = ({ track }: { track: Track }): JSX.Element => {
const { src, title } = track;
return (
<audio id={"audio_" + title}>
<source src={src} />
Your browser does not support the <code>audio</code> element.
</audio>
);
};
In my redux store, when a user click a Track, I find the audio element associated with the track and use audioElement.play():
const playTrack = (track: Track) => {
//get all the audio elements for all the playlist tracks
allAudioElems.current = tracks
.map((t) => "audio_" + t.title)
.map((id) => document.getElementById(id) as HTMLAudioElement)
.filter((e) => e !== null);
if (allAudioElems.current) {
allAudioElems.current.forEach((element) => {
//if the audio elements id matches the active track title, then play
if (element.id === "audio_" + track.title) {
element.play();
setCurrentAudio(element);
} else {
element.pause();
}
});
}
setCurrentTrack(track.title);
setIsPlayingAction(true);
setIsPlaying(true);
};
This all works fine on desktop for Chrome/Safari/Firefox, but fails on iOS Safari. Enabling controls, or autoPlay doesn't seem to do anything either.
I know that .play() returns a promise, but the error message is very vague when I catch the .play() error:
element.play().then((e)=>{
console.log("played audio!");
}).catch((error)=>{
console.log(error)
});
Gives Error:
Unhandeled Promise Rejection: AbortError: The operation was aborted
So what's one to do to get audio working on iOS?

S3 video not streaming on iOS with React

As the title says I have some video saved on an s3 bucket. I set my nodejs to stream it on my react app. It works fine on all devices except iOS. I did some searches and I can't find the issue. The server is returning the initial bytes request as 206. I checked the headers but I can't find the issue:
Here is my nodejs:
After it reaches the path:
if (!range) {
res.status(400).send("returning err!");
return;
}
s3.headObject({
Bucket: process.env.AWS_BUCKET_NAME,
Key: req.params.key
}, function (err, data) {
if (err) {
// an error occurred
console.error(err);
return res.status(500).send("returning err");
}
const videoSize = Number(data.ContentLength);
const CHUNK_SIZE = 10 ** 6; // 1MB
const start = Number(range.replace(/\D/g, ""));
const end = Math.min(start + CHUNK_SIZE, videoSize - 1);
var params = {
Bucket: process.env.AWS_BUCKET_NAME,
Key: req.params.key,
Range: range,
};
var stream = s3.getObject(params).createReadStream();
res.status(206);
res.set('Content-Type', data.ContentType);
res.set('Content-Disposition','inline');
res.set('Accept-Ranges','bytes');
res.set('Accept-Encoding', 'Identity');
res.set('Content-Range', 'bytes ' + start + '-' + end + '/' + videoSize);
res.set('Content-Length', data.ContentLength);
res.set('X-Playback-Session-Id', req.header('X-Playback-Session-Id')); // Part of the attempt to fix
res.set('Connection', 'keep-alive');
res.set('Last-Modified', data.LastModified);
res.set('ETag', data.ETag);
stream.pipe(res);
Here is my Frontend React player code:
<ReactPlayer
ref={player}
// onProgress={onProgress}
playsinline={true}
url={[{ src: source, type: 'video/mp4;' }]} // video location
controls // gives the front end video controls
width='100%'
className='react-player'
height='100%'
allow='autoplay; encrypted-media'
allowFullScreen
// muted={true}
playing={playing}
onPlay={() => setPlaying(true)}
// onPause={() => setPlaying(false)} //part of the attempt to fix
// onSeek={(seek) => playerSeeker(seek)} //part of the attempt to fix
config={{
file: {
attributes: {
controlsList: 'nodownload'
}
}
}}
onContextMenu={e => e.preventDefault()}
onEnded={(e) => onVideoEnded(e)}
onError={(e) => onVideoError(e)}
/>
Again, the first request on iOS is returning a 206 success but node always ends the stream before it even start playing.
Turns out It was just the
res.set('Content-Length', data.ContentLength);
Instead of sending the full length for the video, I needed to return the length of the calculated range.

Azure Media Player on iOS devices

I'm streaming O365 videos using Azure Media Player in a web app that must be used only in mobile devices. It works with WP and Android, but the player stuck on iOS.
This is my code
client.DefaultRequestHeaders.Add("Authorization", "Bearer " + bearer);
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
var response = await client.GetAsync($"{url}/GetPlaybackUrl('1')");
var content = await response.Content.ReadAsStringAsync();
var firstVal = JsonConvert.DeserializeObject<VideoToken>(content);
response = await client.GetAsync($"{url}/GetStreamingKeyAccessToken");
content = await response.Content.ReadAsStringAsync();
var secondVal = JsonConvert.DeserializeObject<VideoToken>(content);
Client Side
<video id="newsVideoAMP" class="azuremediaplayer amp-default-skin amp-big-play-centered" tabindex="0"></video>
var initVideoPlayer = function (playbackUrl, streamingKeyAccessToken) {
try {
var myOptions = {
"nativeControlsForTouch": false,
controls: true,
autoplay: false,
techOrder: ["azureHtml5JS", "flashSS", "html5FairPlayHLS", "silverlightSS", "html5"],
logo: { enabled: false }
}
newsVideoPlayer = amp("newsVideoAMP", myOptions,
function () {
this.addEventListener(amp.eventName.error, function () {
window.alert('error');
console.log('Error: amp init');
var errorDetails = newsVideoPlayer.error();
window.alert(errorDetails);
var code = errorDetails.code;
var message = errorDetails.message;
$("#log").append("<li><span>code: " + code + " - detail: " + message + "</span></li>')");
});
});
newsVideoPlayer.src([
{
"src": playbackUrl,
"type": "application/vnd.ms-sstr+xml",
"protectionInfo": [
{
"type": "AES",
"authenticationToken": streamingKeyAccessToken
}
]
}]);
}
catch (err) {
console.log(err);
}
}
I think the issue is related to video encoding. So I tried to use GetPlaybackUrl('0') (and avoid the next token request), but the player stops to work on WP and Android and still not work on iOS.
The logger in callback function doesn't tell me some useful and I have also tried to change the tech order.
Is there a console to manage video encoding in order to avoid the AES algorithm and the decrypt token? Because this doc explain that iOS works with HTML5 tech will no token request. How can I solve? Thanks
I found a workaround to reproduce videos on iOS devices.
Instead of use REST API I put an iframe element in my page with the video embedded.
Like this (using MVC Razor):
var url = "{YourWebsiteUrl}/portals/hub/_layouts/15/VideoEmbedHost.aspx?chId={YourChannelId}&vId={YourVideoId}&width=853&height=480&autoPlay=false&showInfo=false";
<iframe width=853 height=480 id="videoframe" src="#url" allowfullscreen data-spresponsive style='position: absolute; top: 0; left: 0; right: 0; bottom: 0; height: 100%; max-width: 100%;'></iframe>
I get this code from the popup of "embed" menu in the video page of Office365 Video.
If somebody else knows another (and better) method, please let me know. Thanks

Cordova iOS Video tag Local File Source

I have problem playing local video on iOS on my Cordova based app. At the beginning I want to stress out that this problem is happening only when I'm using WKWebView, and if UiWebView is used, video plays fine. This is scenario I have:
-User comes to screen to which video url is passed
-Via FileTransfer I download it to phone and store it at desired location
-Using JS video is loaded to <video> tag and played.
Basically I'm doing everything as described in answer to this SO question.
The problem with UiWebView was that if relative path was set to src, video for some reason couldn't be loaded (no matter which combination I used), so this solution worked great for me, because it is based on this line of code:
entry.toURL()
This returns full path of the downloaded video which is great, at least for the UiWebView.
The problem for WkWebView is that entry.toURL() returns smth. like this:
file:///var/mobile/Containers/Data/Application/3A43AFB5-BEF6-4A0C-BBDB-FC7D2D98BEE9/Documents/videos/Dips.mp4
And WKWebView doesn't work with file:// protocol. Also, neither WKWebView works wit relative paths :(
Can anyone help me to fix this ?
I got this working today with the following but only when deployed to my device in Release mode. When deploying the app in Debug mode to my device it would not work.
iOS 9.3.2
Cordova 4.0.0 (iOS 3.8.0)
Telerik WKWebView Polyfill 0.6.9
Video list load method:
var path = window.cordova.file.documentsDirectory, //iTunes File Sharing directory
href = 'http://localhost:12344/Documents', //WKWebView default server url to documents
list = [];
function fsSuccess(dir) {
var reader = dir.createReader();
reader.readEntries(function (entries) {
for (var i = 0; i < entries.length; i++) {
list.push({ name: entries[i].name, path: href + entries[i].fullPath });
}
});
}
function fsError(error) {
console.log('error', error)
}
window.resolveLocalFileSystemURL(path, fsSuccess, fsError);
Video list click handler:
var video = $('#video')[0],
source = $('#source');
function play(index) {
source.attr('src', list[index].path);
video.load();
video.play();
}
Video player markup:
<video id="video" autoplay controls loop webkit-playsinline>
<source id="source" type="video/mp4" />
</video>
I was banging my head on my desk a la Ren Hoek while debugging until I attempted a release buid and it worked.
Sample snippet that uses cordova file opener plugin to open the download file from device.(Not tested in WKWebView though)
var fileTransfer = new FileTransfer();
var cdr;
if (sessionStorage.platform.toLowerCase() == "android") {
window.resolveLocalFileSystemURL(cordova.file.externalRootDirectory, onFileSystemSuccess, onError);
} else {
// for iOS
window.requestFileSystem(LocalFileSystem.PERSISTENT, 0, onFileSystemSuccess, onError);
}
function onError(e) {
navigator.notification.alert("Error : Downloading Failed");
};
function onFileSystemSuccess(fileSystem) {
var entry = "";
if (sessionStorage.platform.toLowerCase() == "android") {
entry = fileSystem;
} else {
entry = fileSystem.root;
}
entry.getDirectory("Cordova", {
create: true,
exclusive: false
}, onGetDirectorySuccess, onGetDirectoryFail);
};
function onGetDirectorySuccess(dir) {
cdr = dir;
dir.getFile(filename, {
create: true,
exclusive: false
}, gotFileEntry, errorHandler);
};
function gotFileEntry(fileEntry) {
// URL in which the pdf is available
var documentUrl = "http://localhost:8080/testapp/test.pdf";
var uri = encodeURI(documentUrl);
fileTransfer.download(uri, cdr.nativeURL + "test.pdf",
function(entry) {
// Logic to open file using file opener plugin
openFile();
},
function(error) {
navigator.notification.alert(ajaxErrorMsg);
},
false
);
};
function openFile() {
cordova.plugins.fileOpener2.open(
cdr.nativeURL + "test.pdf",
"application/pdf", //mimetype
{
error: function(e) {
navigator.notification.alert("Error Opening the File.Unsupported document format.");
},
success: function() {
// success callback handler
}
}
);
};

Distorted audio in iOS 7.1 with WebAudio API

On iOS 7.1, I keep getting a buzzing / noisy / distorted sound when playing back audio using the Web Audio API. It sounds distorted like this, in place of normal like this.
The same files are fine when using HTML5 audio. It all works fine on desktop (Firefox, Chrome, Safari.)
EDIT:
The audio is distorted in the iOS Simulator versions iOS 7.1, 8.1, 8.2. The buzzing sound often starts before I even playback anything.
The audio is distorted on a physical iPhone running iOS 7.1, in both Chrome and Safari.
The audio is fine on a physical iPhone running iOS 8.1, in both Chrome and Safari.
i.e.: the buzzing audio is on iOS 7.1. only.
Howler.js is not the issue. The problem is still there using pure JS like so:
var context;
var sound;
var extension = '.' + ( new Audio().canPlayType( 'audio/ogg' ) !== '' ? 'ogg' : 'mp3');
/** Test for WebAudio API support **/
try {
// still needed for Safari
window.AudioContext = window.AudioContext || window.webkitAudioContext;
// create an AudioContext
context = new AudioContext();
} catch(e) {
// API not supported
throw new Error( 'Web Audio API not supported.' );
}
function loadSound( url ) {
var request = new XMLHttpRequest();
request.open( 'GET', url, true );
request.responseType = 'arraybuffer';
request.onload = function() {
// request.response is encoded... so decode it now
context.decodeAudioData( request.response, function( buffer ) {
sound = buffer;
}, function( err ) {
throw new Error( err );
});
}
request.send();
}
function playSound(buffer) {
var source = context.createBufferSource();
source.buffer = buffer;
source.connect(context.destination);
source.start(0);
}
loadSound( '/tests/Assets/Audio/En-us-hello' + extension );
$(document).ready(function(){
$( '#clickme' ).click( function( event ) {
playSound(sound);
});
}); /* END .ready() */
A live version of this code is available here: Web Audio API - Hello world
Google did not bring up any result about such a distorted sound issue on iOS 7.1.
Has anyone else run into it? Should I file a bug report to Apple?
I believe the issue is caused due to resetting the audioContext.sampleRate prop, which seem to happen after the browser/OS plays something recorded in a different sampling rate.
I've devised the following workaround, which basically silently plays a short wav file recorded in the sampling rate that the device currently does playback on:
"use strict";
var getData = function( context, filePath, callback ) {
var source = context.createBufferSource(),
request = new XMLHttpRequest();
request.open( "GET", filePath, true );
request.responseType = "arraybuffer";
request.onload = function() {
var audioData = request.response;
context.decodeAudioData(
audioData,
function( buffer ) {
source.buffer = buffer;
callback( source );
},
function( e ) {
console.log( "Error with decoding audio data" + e.err );
}
);
};
request.send();
};
module.exports = function() {
var AudioContext = window.AudioContext || window.webkitAudioContext,
context = new AudioContext();
getData(
context,
"path/to/short/file.wav",
function( bufferSource ) {
var gain = context.createGain();
gain.gain.value = 0;
bufferSource.connect( gain );
gain.connect( context.destination );
bufferSource.start( 0 );
}
);
};
Obviously, if some of the devices have different sampling rates, you would need to detect and use a specific file for every rate.
it looks like iOS6+ Safari defaults to a sample rate of 48000. If you type this into the developer console when you first open mobile safari, you'll get 48000:
var ctx = new window.webkitAudioContext();
console.log(ctx.sampleRate);
Further Reference: https://forums.developer.apple.com/thread/20677
Then if you close the initial context on load: ctx.close(), the next created context will use the sample rate most other browsers use (44100) and sound will play without distortion.
Credit to this for pointing me in the right direction (and in case the above no longer works in the future): https://github.com/Jam3/ios-safe-audio-context/blob/master/index.js
function as of post date:
function createAudioContext (desiredSampleRate) {
var AudioCtor = window.AudioContext || window.webkitAudioContext
desiredSampleRate = typeof desiredSampleRate === 'number'
? desiredSampleRate
: 44100
var context = new AudioCtor()
// Check if hack is necessary. Only occurs in iOS6+ devices
// and only when you first boot the iPhone, or play a audio/video
// with a different sample rate
if (/(iPhone|iPad)/i.test(navigator.userAgent) &&
context.sampleRate !== desiredSampleRate) {
var buffer = context.createBuffer(1, 1, desiredSampleRate)
var dummy = context.createBufferSource()
dummy.buffer = buffer
dummy.connect(context.destination)
dummy.start(0)
dummy.disconnect()
context.close() // dispose old context
context = new AudioCtor()
}
return context
}

Resources