Twilio Remote video from Safari iPhone showing black-screen in Angular 5 - ios

iPhone safari version: 11.2.5
Chrome version: Checking for updates Version 80.0.3987.163 (Official Build) (64-bit)
The connection is a peer to peer.
If I can give you an overview it's like -
Video is coming on iPhone from mac safari, Chrome, window laptop
Video is coming on Android Phone from mac safari, Chrome, window laptop
Video is coming on mac safari, Chrome, window laptop from Android Phone
Video is NOT COMING on mac safari, Chrome, window laptop from iPhone
Please let me know if you can help. Thanks in advance.

I solve this issue
createLocalVideoTrack({
video: { width: 1280, height: 720 },
}).then(track => {
if (this.localVideo) {
const element = track.attach();
this.renderer.data.id = track.sid;
this.renderer.setStyle(element, 'width', '25%');
this.renderer.appendChild(this.localVideo.nativeElement, element);
this.attachVideoClass();
}
});
Should be :
this.roomObj.localParticipant.videoTracks.forEach(publication => {
const element = publication.track.attach();
this.renderer.data.id = publication.track.sid;
this.renderer.setStyle(element, 'width', '25%');
this.renderer.appendChild(this.localVideo.nativeElement, element);
})

Twilio developer evangelist here.
I would recommend reading through the developing for Safari 11 article which goes over a bunch of potential issues.

Please check code:
import { Injectable, EventEmitter, ElementRef, Renderer2, RendererFactory2 } from '#angular/core';
import { Observable } from 'rxjs/Observable';
import { Observer } from 'rxjs';
import { connect, createLocalTracks, createLocalVideoTrack } from 'twilio-video';
import { BehaviorSubject } from 'rxjs/BehaviorSubject';
import { Http } from '#angular/http';
import { Router } from '#angular/router';
import { BaCustomPreLoader } from './baCustomPreloader.service';
#Injectable()
export class TwilioService {
remoteVideo: ElementRef;
localVideo: ElementRef;
previewing: boolean;
msgSubject = new BehaviorSubject("");
roomObj: any;
roomParticipants;
private renderer: Renderer2;
constructor(
private http: Http,
private router: Router,
private rendererFactory: RendererFactory2,
private baCustomPreLoader: BaCustomPreLoader) {
this.renderer = rendererFactory.createRenderer(null, null);
}
getToken(username): Observable<any> {
return this.http.post('/abc', { uid: 'ashish' });
}
microphone = true;
mute() {
this.roomObj.localParticipant.audioTracks.forEach(function (
audioTrack
) {
audioTrack.track.disable();
});
this.microphone = false;
}
unmute() {
this.roomObj.localParticipant.audioTracks.forEach(function (
audioTrack,
key,
map
) {
audioTrack.track.enable();
});
this.microphone = true;
}
connectToRoom(accessToken: string, options): void {
connect(accessToken, options).then(room => {
this.roomObj = room;
if (!this.previewing && options['video']) {
this.startLocalVideo();
this.previewing = true;
}
this.roomParticipants = room.participants;
room.participants.forEach(participant => {
this.attachParticipantTracks(participant);
});
room.on('participantDisconnected', (participant) => {
this.detachParticipantTracks(participant);
});
room.on('participantConnected', (participant) => {
this.roomParticipants = room.participants;
this.attachParticipantTracks(participant);
participant.on('trackAdded', track => {
const element = track.attach();
this.renderer.data.id = track.sid;
this.renderer.setStyle(element, 'height', '100%');
this.renderer.setStyle(element, 'max-width', '100%');
this.renderer.appendChild(this.remoteVideo.nativeElement, element);
this.attachVideoClass();
this.baCustomPreLoader.hide();
});
});
// When a Participant adds a Track, attach it to the DOM.
room.on('trackAdded', (track, participant) => {
this.attachTracks([track]);
});
// When a Participant removes a Track, detach it from the DOM.
room.on('trackRemoved', (track, participant) => {
this.detachTracks([track]);
});
room.once('disconnected', room => {
room.localParticipant.tracks.forEach(track => {
track.track.stop();
const attachedElements = track.track.detach();
attachedElements.forEach(element => element.remove());
room.localParticipant.videoTracks.forEach(video => {
const trackConst = [video][0].track;
trackConst.stop(); // <- error
trackConst.detach().forEach(element => element.remove());
room.localParticipant.unpublishTrack(trackConst);
});
let element = this.remoteVideo.nativeElement;
while (element.firstChild) {
element.removeChild(element.firstChild);
}
let localElement = this.localVideo.nativeElement;
while (localElement.firstChild) {
localElement.removeChild(localElement.firstChild);
}
this.router.navigate(['thanks']);
setTimeout(() => {
window.location.reload();
}, 1000)
});
});
}, (error) => {
alert(error.message);
});
}
attachParticipantTracks(participant): void {
participant.tracks.forEach(part => {
this.trackPublished(part);
});
}
trackPublished(publication) {
if (publication.isSubscribed)
this.attachTracks(publication.track);
if (!publication.isSubscribed)
publication.on('subscribed', track => {
this.attachTracks(track);
});
}
attachTracks(tracks) {
const element = tracks.attach();
this.renderer.data.id = tracks.sid;
this.renderer.setStyle(element, 'height', '100%');
this.renderer.setStyle(element, 'max-width', '100%');
this.renderer.appendChild(this.remoteVideo.nativeElement, element);
this.attachVideoClass();
this.baCustomPreLoader.hide();
}
startLocalVideo(): void {
createLocalVideoTrack({
video: { width: 1280, height: 720 },
}).then(track => {
if (this.localVideo) {
const element = track.attach();
this.renderer.data.id = track.sid;
this.renderer.setStyle(element, 'width', '25%');
this.renderer.appendChild(this.localVideo.nativeElement, element);
this.attachVideoClass();
}
});
}
detachParticipantTracks(participant) {
this.detachTracks(participant);
}
detachTracks(tracks): void {
tracks.tracks.forEach(track => {
let element = this.remoteVideo.nativeElement;
while (element.firstChild) {
element.removeChild(element.firstChild);
}
});
}
attachVideoClass() {
let remote = document.getElementById('remote');
}
}

Related

How to pass geolocation permission to react-native-webview?

When using the geolcoation api in a react-native-webview, I am asked twice whether the app is allowed to use my current location. How is it possible to forward the already (not) granted permission to the webview?
I am currently using react-native 0.68 and react-native-webview 11.22.
First prompt:
Second prompt:
I should only be asked once for permission to use my current geolocation.
In case somebody faces the same problem, I solved this issue with the following workaround. I injected a custome javacscript into the webview to override the used geolocation api in the webview. My custome script does all the communication with the app. The app returns the geolocation and so the webview doesn't need to aks for permission.
Custome Script
export const getGeoLocationJS = () => {
const getCurrentPosition = `
navigator.geolocation.getCurrentPosition = (success, error, options) => {
window.ReactNativeWebView.postMessage(JSON.stringify({ event: 'getCurrentPosition', options: options }));
window.addEventListener('message', (e) => {
let eventData = {}
try {
eventData = JSON.parse(e.data);
} catch (e) {}
if (eventData.event === 'currentPosition') {
success(eventData.data);
} else if (eventData.event === 'currentPositionError') {
error(eventData.data);
}
});
};
true;
`;
const watchPosition = `
navigator.geolocation.watchPosition = (success, error, options) => {
window.ReactNativeWebView.postMessage(JSON.stringify({ event: 'watchPosition', options: options }));
window.addEventListener('message', (e) => {
let eventData = {}
try {
eventData = JSON.parse(e.data);
} catch (e) {}
if (eventData.event === 'watchPosition') {
success(eventData.data);
} else if (eventData.event === 'watchPositionError') {
error(eventData.data);
}
});
};
true;
`;
const clearWatch = `
navigator.geolocation.clearWatch = (watchID) => {
window.ReactNativeWebView.postMessage(JSON.stringify({ event: 'clearWatch', watchID: watchID }));
};
true;
`;
return `
(function() {
${getCurrentPosition}
${watchPosition}
${clearWatch}
})();
`;
};
Webview
import Geolocation from '#react-native-community/geolocation';
let webview = null;
<WebView
geolocationEnabled={ true }
injectedJavaScript={ getGeoLocationJS() }
javaScriptEnabled={ true }
onMessage={ event => {
let data = {}
try {
data = JSON.parse(event.nativeEvent.data);
} catch (e) {
console.log(e);
}
if (data?.event && data.event === 'getCurrentPosition') {
Geolocation.getCurrentPosition((position) => {
webview.postMessage(JSON.stringify({ event: 'currentPosition', data: position }));
}, (error) => {
webview.postMessage(JSON.stringify({ event: 'currentPositionError', data: error }));
}, data.options);
} else if (data?.event && data.event === 'watchPosition') {
Geolocation.watchPosition((position) => {
webview.postMessage(JSON.stringify({ event: 'watchPosition', data: position }));
}, (error) => {
webview.postMessage(JSON.stringify({ event: 'watchPositionError', data: error }));
}, data.options);
} else if (data?.event && data.event === 'clearWatch') {
Geolocation.clearWatch(data.watchID);
}
}}
ref={ ref => {
webview = ref;
if (onRef) {
onRef(webview)
}
}}
source={ url }
startInLoadingState={ true }
/>

Quasar File Picker doesn't upload image in ios

I want to implement image uploading. It works for web (Chrome/Safari), but not for mobile ios (built with Capacitor). It shows image selection, but when I select it nothing happens.
Here is my input:
<q-file
v-model="avatarFile"
accept=".jpg, .png"
#input="uploadImage"
/>
async uploadImage() {
if (this.avatarFile) {
let extension = "jpg";
if (this.avatarFile.type === "image/png") {
extension = "png";
}
try {
this.loading = 1;
const {
data: {
uploadUrl: { uploadUrl },
},
} = await this.$apollo.query({
query: GET_UPLOAD_URL,
variables: {
extension,
},
});
await axios.put(uploadUrl, this.avatarFile);
await setTimeout(() => {
this.$emit("on-image-upload");
this.loading = 0;
}, 3000);
} catch (e) {
this.alertError(e);
this.loading = 0;
}
}
}
What am I doing wrong? Thanx in advance!
Found the solution. My accept rule had the wrong format. This works:
accept="image/*"

WebRTC - how to switch between getUserMedia and getDisplayMedia tracks inside RTCPeerConnection

I'm trying to develop an app where users can can video call to each other and share their screens using WebRTC technology. I have succeed with either video call or screen sharing app and now I'm trying to make it to be able to switch between getUserMedia and getDisplayMedia on button click during a call inside the same RTCPeerConnection but it doesn't work.
This is how I thought it could work:
function onLogin(success) {
var configuration = { offerToReceiveAudio: true, offerToReceiveVideo: true, "iceServers" : [ { "url" : "stun:stun.1.google.com:19302" } ] };
myConnection = window.RTCPeerConnection ? new RTCPeerConnection(configuration, { optional: [] }) : new RTCPeerConnection(configuration, { optional: [] });
myConnection.onicecandidate = function (event) {
console.log("onicecandidate");
if (event.candidate) send({ type: "candidate", candidate: event.candidate });
};
myConnection.ontrack=function(e){
try{remoteVideo.src = window.webkitURL?window.webkitURL.createObjectURL(e.streams[0]):window.URL.createObjectURL(e.streams[0])}
catch(err){remoteVideo.srcObject=e.streams[0]}
}
myConnection.ondatachannel=openDataChannel
openDataChannel();
startAVStream();
//startSStream()
};
function startAVStream(enable){
if(sStream)sStream.getTracks().forEach( function (track) {
try{myConnection.removeTrack( track, sStream );}
catch(e){}
} );
navigator.mediaDevices.getUserMedia({ video: true, audio: true }).then(s => {
if(!avStream){
avStream = s;
avStream.getTracks().forEach( function (track) {
myConnection.addTrack( track, avStream );
} );
}
}, function (error) { console.log(error); });
}
function startSStream(enable){
if(avStream)avStream.getTracks().forEach( function (track) {
try{myConnection.removeTrack( track, avStream );}
catch(e){}
} );
navigator.mediaDevices.getDisplayMedia({ video: true }).then(s => {
if(!sStream){
sStream = s;
sStream.getTracks().forEach( function (track) {
myConnection.addTrack( track, sStream );
} );
}
}, function (error) { console.log(error); });
}
Can anyone tell me how I can switch between tracks inside the same RTCPeerConnection or should I create 2 separate RTCPeerConnection - one for video/audio streaming and another for screen sharing?
Any help appreciated! Thanks!
You could use RTCRtpSender.replaceTrack to splice the screen capture track. This doesn't require renegotiation, and therefore has very low latency.
let newstream = navigator.mediaDevices.getDisplayMedia({});
let newtrack = newstream.getTracks()[1];
if(newtrack.kind !== 'video')
throw new Error('Eek!?');
pc.getSenders().forEach(async s => {
if(s.track && s.track.kind === 'video')
await s.replaceTrack(newtrack);
});
The test for s.track not being null deals with the case where you previously called replaceTrack(..., null).
shareScreen = () =>{
const success = (stream) => {
window.localStream = stream
// this.localVideoref.current.srcObject = stream
// localStream.replaceStream(stream);
this.setState({
localStream: stream
})
Object.values(this.state.peerConnections).forEach(pc => {
pc.getSenders().forEach(async s => {
console.log("s.track ",s.track);
if(s.track && s.track.kind === 'video'){
stream.getTracks().forEach(track => {
// pc.addTrack(track, this.state.localStream)
s.replaceTrack(track);
});
}
});
});
}
const failure = (e) => {
console.log('getUserMedia Error: ', e)
}
navigator.mediaDevices.getDisplayMedia({ cursor: true }).then(success).catch(failure)}

IOS Notification Permission alert does not show

SDK Version: 39.0.0
Platforms(Android/iOS/web/all): All
I am not getting accept or decline notifications permissions alert when loading my app in production.
I have tried clearing certificates and keys and allowing expo to add everything from a clean slate, but still no luck. I am starting to think maybe it’s my code which is the reason why the alert doesn’t get fired.
import Constants from "expo-constants";
import * as Notifications from "expo-notifications";
import { Permissions } from "expo-permissions";
import { Notifications as Notifications2 } from "expo";
Notifications.setNotificationHandler({
handleNotification: async () => ({
shouldShowAlert: true,
shouldPlaySound: true,
shouldSetBadge: false
})
});
export default class LoginScreen extends React.Component {
state = {
email: "",
password: "",
notification: {},
errorMessage: null
};
async componentDidMount() {
this.registerForPushNotificationsAsync();
//Notifications.addNotificationReceivedListener(this._handleNotification);
Notifications2.addListener(data => {
this.setState({ notification: data });
});
Notifications.addNotificationResponseReceivedListener(
this._handleNotificationResponse
);
}
_handleNotification = notification => {
this.setState({ notification: notification });
};
_handleNotificationResponse = response => {
console.log(response);
};
handleLogin = async () => {
try {
const { email, password } = this.state;
const expoPushToken = await Notifications.getExpoPushTokenAsync();
console.log(expoPushToken);
const userinfo = await firebase
.auth()
.signInWithEmailAndPassword(email, password);
console.log("user ID ", userinfo.user.uid);
await firebase
.firestore()
.collection("users")
.doc(userinfo.user.uid.toString())
.update({
expo_token: expoPushToken["data"]
})
.then(function() {
console.log("token successfully updated!");
})
.catch(function(error) {
// The document probably doesn't exist.
console.error("Error updating document: ", error);
});
} catch (error) {
console.log("=======Error in login", error);
this.setState({ errorMessage: error.message });
}
};
registerForPushNotificationsAsync = async () => {
if (Constants.isDevice) {
const { status: existingStatus } = await Permissions.getAsync(
Permissions.NOTIFICATIONS
);
let finalStatus = existingStatus;
if (existingStatus !== "granted") {
const { status } = await Permissions.askAsync(
Permissions.NOTIFICATIONS
);
finalStatus = status;
}
if (finalStatus !== "granted") {
alert("Failed to get push token for push notification!");
return;
}
const token = await Notifications.getExpoPushTokenAsync();
console.log(token);
//this.setState({ expoPushToken: token });
} else {
alert("Must use physical device for Push Notifications");
}
if (Platform.OS === "android") {
Notifications.setNotificationChannelAsync("default", {
name: "default",
importance: Notifications.AndroidImportance.MAX,
vibrationPattern: [0, 250, 250, 250],
lightColor: "#FF231F7C"
});
}
};
import { Permissions } from "expo-permissions";
should of been
import * as Permissions from 'expo-permissions';
Sometimes we all make simple mistakes.

Remote video from Safari iPhone showing black-screen [duplicate]

iPhone safari version: 11.2.5
Chrome version: Checking for updates Version 80.0.3987.163 (Official Build) (64-bit)
The connection is a peer to peer.
If I can give you an overview it's like -
Video is coming on iPhone from mac safari, Chrome, window laptop
Video is coming on Android Phone from mac safari, Chrome, window laptop
Video is coming on mac safari, Chrome, window laptop from Android Phone
Video is NOT COMING on mac safari, Chrome, window laptop from iPhone
Please let me know if you can help. Thanks in advance.
I solve this issue
createLocalVideoTrack({
video: { width: 1280, height: 720 },
}).then(track => {
if (this.localVideo) {
const element = track.attach();
this.renderer.data.id = track.sid;
this.renderer.setStyle(element, 'width', '25%');
this.renderer.appendChild(this.localVideo.nativeElement, element);
this.attachVideoClass();
}
});
Should be :
this.roomObj.localParticipant.videoTracks.forEach(publication => {
const element = publication.track.attach();
this.renderer.data.id = publication.track.sid;
this.renderer.setStyle(element, 'width', '25%');
this.renderer.appendChild(this.localVideo.nativeElement, element);
})
Twilio developer evangelist here.
I would recommend reading through the developing for Safari 11 article which goes over a bunch of potential issues.
Please check code:
import { Injectable, EventEmitter, ElementRef, Renderer2, RendererFactory2 } from '#angular/core';
import { Observable } from 'rxjs/Observable';
import { Observer } from 'rxjs';
import { connect, createLocalTracks, createLocalVideoTrack } from 'twilio-video';
import { BehaviorSubject } from 'rxjs/BehaviorSubject';
import { Http } from '#angular/http';
import { Router } from '#angular/router';
import { BaCustomPreLoader } from './baCustomPreloader.service';
#Injectable()
export class TwilioService {
remoteVideo: ElementRef;
localVideo: ElementRef;
previewing: boolean;
msgSubject = new BehaviorSubject("");
roomObj: any;
roomParticipants;
private renderer: Renderer2;
constructor(
private http: Http,
private router: Router,
private rendererFactory: RendererFactory2,
private baCustomPreLoader: BaCustomPreLoader) {
this.renderer = rendererFactory.createRenderer(null, null);
}
getToken(username): Observable<any> {
return this.http.post('/abc', { uid: 'ashish' });
}
microphone = true;
mute() {
this.roomObj.localParticipant.audioTracks.forEach(function (
audioTrack
) {
audioTrack.track.disable();
});
this.microphone = false;
}
unmute() {
this.roomObj.localParticipant.audioTracks.forEach(function (
audioTrack,
key,
map
) {
audioTrack.track.enable();
});
this.microphone = true;
}
connectToRoom(accessToken: string, options): void {
connect(accessToken, options).then(room => {
this.roomObj = room;
if (!this.previewing && options['video']) {
this.startLocalVideo();
this.previewing = true;
}
this.roomParticipants = room.participants;
room.participants.forEach(participant => {
this.attachParticipantTracks(participant);
});
room.on('participantDisconnected', (participant) => {
this.detachParticipantTracks(participant);
});
room.on('participantConnected', (participant) => {
this.roomParticipants = room.participants;
this.attachParticipantTracks(participant);
participant.on('trackAdded', track => {
const element = track.attach();
this.renderer.data.id = track.sid;
this.renderer.setStyle(element, 'height', '100%');
this.renderer.setStyle(element, 'max-width', '100%');
this.renderer.appendChild(this.remoteVideo.nativeElement, element);
this.attachVideoClass();
this.baCustomPreLoader.hide();
});
});
// When a Participant adds a Track, attach it to the DOM.
room.on('trackAdded', (track, participant) => {
this.attachTracks([track]);
});
// When a Participant removes a Track, detach it from the DOM.
room.on('trackRemoved', (track, participant) => {
this.detachTracks([track]);
});
room.once('disconnected', room => {
room.localParticipant.tracks.forEach(track => {
track.track.stop();
const attachedElements = track.track.detach();
attachedElements.forEach(element => element.remove());
room.localParticipant.videoTracks.forEach(video => {
const trackConst = [video][0].track;
trackConst.stop(); // <- error
trackConst.detach().forEach(element => element.remove());
room.localParticipant.unpublishTrack(trackConst);
});
let element = this.remoteVideo.nativeElement;
while (element.firstChild) {
element.removeChild(element.firstChild);
}
let localElement = this.localVideo.nativeElement;
while (localElement.firstChild) {
localElement.removeChild(localElement.firstChild);
}
this.router.navigate(['thanks']);
setTimeout(() => {
window.location.reload();
}, 1000)
});
});
}, (error) => {
alert(error.message);
});
}
attachParticipantTracks(participant): void {
participant.tracks.forEach(part => {
this.trackPublished(part);
});
}
trackPublished(publication) {
if (publication.isSubscribed)
this.attachTracks(publication.track);
if (!publication.isSubscribed)
publication.on('subscribed', track => {
this.attachTracks(track);
});
}
attachTracks(tracks) {
const element = tracks.attach();
this.renderer.data.id = tracks.sid;
this.renderer.setStyle(element, 'height', '100%');
this.renderer.setStyle(element, 'max-width', '100%');
this.renderer.appendChild(this.remoteVideo.nativeElement, element);
this.attachVideoClass();
this.baCustomPreLoader.hide();
}
startLocalVideo(): void {
createLocalVideoTrack({
video: { width: 1280, height: 720 },
}).then(track => {
if (this.localVideo) {
const element = track.attach();
this.renderer.data.id = track.sid;
this.renderer.setStyle(element, 'width', '25%');
this.renderer.appendChild(this.localVideo.nativeElement, element);
this.attachVideoClass();
}
});
}
detachParticipantTracks(participant) {
this.detachTracks(participant);
}
detachTracks(tracks): void {
tracks.tracks.forEach(track => {
let element = this.remoteVideo.nativeElement;
while (element.firstChild) {
element.removeChild(element.firstChild);
}
});
}
attachVideoClass() {
let remote = document.getElementById('remote');
}
}

Resources