PeerJS: 1.3.2
Tested on: iOS 15 & 13.
I have the below call service file that implements PeerJS functionality to init, establish and answer video calls.
Calls work as expected across Android devices, macOS and PCs.
However, when attempting to join from an iOS device, we see the following error raised:
NotAllowedError: The request is not allowed by the user agent
or the platform in the current context, possibly because the
user denied permission.
call-service.js:
import { Injectable } from '#angular/core';
import { MatSnackBar } from '#angular/material/snack-bar';
import Peer from 'peerjs';
import { BehaviorSubject, Subject } from 'rxjs';
import { v4 as uuidv4 } from 'uuid';
#Injectable()
export class CallService {
private peer: Peer;
private mediaCall: Peer.MediaConnection;
private localStreamBs: BehaviorSubject<MediaStream> = new BehaviorSubject(null);
public localStream$ = this.localStreamBs.asObservable();
private remoteStreamBs: BehaviorSubject<MediaStream> = new BehaviorSubject(null);
public remoteStream$ = this.remoteStreamBs.asObservable();
private isCallStartedBs = new Subject<boolean>();
public isCallStarted$ = this.isCallStartedBs.asObservable();
constructor(private snackBar: MatSnackBar) { }
public initPeer(): string {
if (!this.peer || this.peer.disconnected) {
const peerJsOptions: Peer.PeerJSOption = {
debug: 3,
config: {
iceServers: [
{
urls: [
'stun:stun1.l.google.com:19302',
'stun:stun2.l.google.com:19302',
],
}]
}
};
try {
let id = uuidv4();
this.peer = new Peer(id, peerJsOptions);
return id;
} catch (error) {
console.error(error);
}
}
}
public async establishMediaCall(remotePeerId: string) {
try {
const stream = await navigator.mediaDevices.getUserMedia({ video: true, audio: true});
let peerOptions: any = {};
if (this.checkSafari()) {
peerOptions.serialization = "json";
}
const connection = this.peer.connect(remotePeerId, peerOptions);
connection.on('error', err => {
console.error(err);
this.snackBar.open(err, 'Close');
});
this.mediaCall = this.peer.call(remotePeerId, stream);
if (!this.mediaCall) {
let errorMessage = 'Unable to connect to remote peer';
this.snackBar.open(errorMessage, 'Close');
throw new Error(errorMessage);
}
this.localStreamBs.next(stream);
this.isCallStartedBs.next(true);
this.mediaCall.on('stream',
(remoteStream) => {
this.remoteStreamBs.next(remoteStream);
});
this.mediaCall.on('error', err => {
this.snackBar.open(err, 'Close');
console.error(err);
this.isCallStartedBs.next(false);
});
this.mediaCall.on('close', () => this.onCallClose());
}
catch (ex) {
console.error(ex);
this.snackBar.open(ex, 'Close');
this.isCallStartedBs.next(false);
}
}
public async enableCallAnswer() {
try {
let peerOptions: any = {};
if (this.checkSafari()) {
peerOptions.serialization = "json";
}
const stream = await navigator.mediaDevices.getUserMedia({ video: true, audio: true });
this.localStreamBs.next(stream);
this.peer.on('call', async (call) => {
this.mediaCall = call;
this.isCallStartedBs.next(true);
this.mediaCall.answer(stream);
this.mediaCall.on('stream', (remoteStream) => {
this.remoteStreamBs.next(remoteStream);
});
this.mediaCall.on('error', err => {
this.snackBar.open(err, 'Close');
this.isCallStartedBs.next(false);
console.error(err);
});
this.mediaCall.on('close', () => this.onCallClose());
});
}
catch (ex) {
console.error(ex);
this.snackBar.open(ex, 'Close');
this.isCallStartedBs.next(false);
}
}
private onCallClose() {
this.remoteStreamBs?.value.getTracks().forEach(track => {
track.stop();
});
this.localStreamBs?.value.getTracks().forEach(track => {
track.stop();
});
this.snackBar.open('Call Ended', 'Close');
}
public closeMediaCall() {
this.mediaCall?.close();
if (!this.mediaCall) {
this.onCallClose()
}
this.isCallStartedBs.next(false);
}
public destroyPeer() {
this.mediaCall?.close();
this.peer?.disconnect();
this.peer?.destroy();
}
public checkSafari() {
let seemsChrome = navigator.userAgent.indexOf("Chrome") > -1;
let seemsSafari = navigator.userAgent.indexOf("Safari") > -1;
return seemsSafari && !seemsChrome;
}
}
Closing. This was a local permissions issue on my test device and no fault of PeerJS.
Reinstalling Chrome on iOS then enabled the relevant camera permissions
Related
I am trying to post a file object to my nuxt 3 api route
Problem is:
Data from client has my file object
Data from server returns empty object
Screenshot of the issue
Where did my file object go?
const handleImageUpload = async (evt: Event) => {
const target = evt.target as HTMLInputElement
if (target.files) {
const file = target.files[0]
const upload: iUpload = {
name: file.name,
type: file.type,
file
}
console.log("data from client", upload)
try {
const { data, error } = await useFetch(constants.imageUploadApiUrl, {
headers: { "Content-type": "application/json" },
method: 'POST',
body: upload
})
console.log("data from server", data.value)
} catch (error) {
console.log(error)
}
}
}
constants.imageUploadApiUrl (api route) has the following
import { getQuery, readBody } from "h3"
import { iUpload } from "~~/helpers/interfaces"
export default defineEventHandler(async (event) => {
try {
const query = getQuery(event)
const body = await readBody(event) as iUpload
return { body }
} catch (error: any) {
return { error: error.message }
}
})
iUpload interface is this
export interface iUpload {
name: string;
type: string;
file: File;
}
I eventually got it working. Meanwhile it's using supabase as it's backend (forgot to mention that).
But here are the changes I made.
#1 - I added a utility function to convert the file to base64 string
export const getBase64 = (file: File) => {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.readAsDataURL(file);
reader.onload = () => resolve(reader.result);
reader.onerror = error => reject(error);
});
}
#2 - I updated the handleImageUpload function like below. The only change being in the file key
const handleImageUpload = async (evt: Event) => {
const target = evt.target as HTMLInputElement
if (target.files) {
const fileObj = target.files[0]
const upload: iUpload = {
path: id(memberName(store.selected), '-'),
name: fileObj.name,
file: await getBase64(fileObj) as string, // <**=**
type: fileObj.type
}
console.log("data from client", upload)
try {
const { data, error } = await useFetch(constants.imageUploadApiUrl, {
headers: { "Content-type": "multipart/form-data" },
method: 'POST',
body: upload
})
console.log("data from server", data.value)
} catch (error) {
console.log(error)
}
}
}
#3 - Furthermore I updated the server route as follows:
export default defineEventHandler(async (event) => {
try {
const body = await readBody(event) as iUpload
const filePath = `${body.path}/${body.name}`
const res = await fetch(body.file)
const blob = await res.blob()
const response = await supabase.storage
.from("pictures")
.upload(filePath, blob, {
contentType: body.type,
upsert: true,
})
return {
data: response.data,
error: response.error?.message,
}
} catch (error: any) {
return { error: error.message }
}
})
#4 - Lastly I updated the policies on supabase storage bucket and storage object to the following:
supabase storage policy update
I am getting the following error when trying to run my react-native app with iOS device and I am unsure why, any ideas? Just a heads up the app works fine on android simulator.
ERROR TypeError: null is not an object (evaluating '_$$_REQUIRE(_dependencyMap[0], "react-native").NativeModules.RNGetRandomValues.getRandomBase64')
I am building a Tik Tok clone and I am trying to publish a video I record on my device to my AWS database.
So far I have tried to:
adding import 'react-native-get-random-values';,
running the app in release mode,
My code doesn't work when I try to publish the video I record on my phone to the database. I get the error posted above and a warning a created letting me know the video hasn't been published. Again I am only having this error on the iOS side of the application.
Here is my code for the screen:
import React, {
useState,
useRef,
useEffect
} from 'react';
import {
View,
Text,
TouchableOpacity,
TextInput,
Button
} from 'react-native';
import styles from '/Users/Documents/TikTok/src/screens/CreatePost/styles.js';
import {
Storage,
API,
graphqlOperation,
Auth
} from 'aws-amplify';
import {
useRoute,
useNavigation
} from '#react-navigation/native';
import {
createPost
} from '/Users/Documents/TikTok/src/graphql/mutations.js';
import {
v4 as uuidv4
} from 'uuid';
const CreatePost = () => {
const [description, setDescription] = useState();
const [videoKey, setVideoKey] = useState();
const route = useRoute();
const navigation = useNavigation();
const uploadToStorage = async(imagePath) => {
try {
const response = await fetch(imagePath);
const blob = await response.blob();
const filename = `${uuidv4()}.mp4`;
const s3Response = await Storage.put(filename, blob);
setVideoKey(s3Response.key);
} catch (e) {
console.error(e);
}
};
useEffect(() => {
uploadToStorage(route.params.videoUri);
}, []);
const onPublish = async() => {
if (!videoKey) {
console.warn("Video is not yet uploaded");
return;
}
try {
const userInfo = await Auth.currentAuthenticatedUser();
const newPost = {
videoUri: videoKey,
description: description,
userID: userInfo.attributes.sub,
songID: '6957a5ce-5f8b-40b0-9f6b-aa68eba19c2b',
};
const response = await API.graphql(
graphqlOperation(createPost, {
input: newPost
}),
);
navigation.navigate("Home", {
screen: "Home"
});
console.warn('Video Uploaded');
} catch (e) {
console.log(e);
}
};
return ( <
View style = {
styles.container
} >
<
TextInput value = {
description
}
onChangeText = {
setDescription
}
numberOfLines = {
5
}
placeholder = {
"Post Description"
}
style = {
styles.textInput
}
/> <
TouchableOpacity onPress = {
onPublish
} >
<
View style = {
styles.button
} >
<
Text style = {
styles.buttonText
} > Publish < /Text> <
/View> <
/TouchableOpacity> <
/View>
);
};
export default CreatePost;
I am trying to get current location in IOS 14, but i am getting no response and when i check in expo
settings it's not showing location permission there. I have checked in both simulator and physical device.
Hook Code
import { useEffect, useState } from "react";
import * as Location from "expo-location";
export default useLocation = () => {
const [location, setLocation] = useState();
const getLocation = async () => {
try {
const { granted } = await Location.requestPermissionsAsync();
if (!granted) return;
const {
coords: { latitude, longitude },
} = await Location.getLastKnownPositionAsync();
setLocation({ latitude, longitude });
} catch (error) {
console.log(error);
}
};
useEffect(() => {
getLocation();
}, []);
return location;
};
Response
undefined
The docs says Location.getLastKnownPositionAsync() might return null:
Returns a promise resolving to an object of type LocationObject or
null if it's not available or doesn't match given requirements such as
maximum age or required accuracy.
so you should do something like:
import { useEffect, useState } from "react";
import * as Location from "expo-location";
export default useLocation = () => {
const [location, setLocation] = useState();
const getLocation = async () => {
try {
const { granted } = await Location.requestPermissionsAsync();
if (!granted) return;
const last = await Location.getLastKnownPositionAsync();
if (last) setLocation(last);
else {
const current = await Location.getCurrentPositionAsync();
setLocation(current);
}
} catch (error) {
console.log(error);
}
};
useEffect(() => {
getLocation();
}, []);
return location;
};
use requestForegroundPermissionAsync() instead of requestPermissionAsync. and the problem is solved.
The cancellation response from token.botframework.com is currently being displayed to screen like this:
{
"error": {
"code": "ServiceError",
"message": "Missing required query string parameter: code. Url = https://token.botframework.com/.auth/web/redirect?state=d48fb60ae4834fd8adabfe054a5eff74&error_description=The+user+chose+not+to+give+your+app+access+to+their+Dropbox+account.&error=access_denied"
}
}
How can I, instead, handle the cancellation gracefully? If the user cancels like this, I'd like to just have the auth-card-popup window close automatically.
This is for an action-type messaging extension app that I'm building. The sign-in process begins with an auth card. The bot is pointed at a Dropbox OAUTH2 connection. Here the relevant code that brings up the card:
const { TeamsActivityHandler, CardFactory } = require('botbuilder');
class MsgExtActionBot extends TeamsActivityHandler {
constructor() {
super();
this.connectionName = 'oauth2-provider';
}
async handleTeamsMessagingExtensionFetchTask(context, action) {
if (!await this.isAuthenticated(context)) {
return this.getSignInResponse(context);
}
}
async isAuthenticated(context) {
let tokenResponse = await context.adapter.getUserToken(
context,
this.connectionName
);
if (tokenResponse && tokenResponse.token) {
return true;
}
if (!context.activity.value.state) {
return false;
}
tokenResponse = await context.adapter.getUserToken(
context,
this.connectionName,
context.activity.value.state
);
if (tokenResponse && tokenResponse.token) {
return true;
}
return false;
}
async getSignInResponse(context) {
const signInLink = await context.adapter.getSignInLink(context, this.connectionName);
return {
composeExtension: {
type: 'auth',
suggestedActions: {
actions: [{
type: 'openUrl',
value: signInLink,
title: 'Please sign in'
}]
},
}
};
}
}
I am using cordova-plugin-media-with-compression in an Ionic 2 app.
On iOS I can record and playback if I pass startRecord() a filename and call that again without changing this.media.
I can't seem to play audio files stored elsewhere in the file system - as I have to pass a new src to startRecord() and this is the bit I think I am doing incorrectly.
import { Component } from '#angular/core';
import { ModalController, LoadingController, ToastController, Platform } from 'ionic-angular';
import { File, FileEntry, Entry, FileError, DirectoryEntry} from 'ionic-native';
declare var Media: any; // stops errors w/ cordova-plugin-media-with-compression types
#Component({
selector: 'page-add-doc',
templateUrl: 'add-doc.html'
})
export class AddDocPage {
isRecording = false;
isRecorded = false;
audioUrl ='';
localAudioUrl = '';
media: any;
newFileName: string;
newFileNameM4A: string;
homerAudio = 'http://techslides.com/demos/samples/sample.m4a'
constructor(private modalCtrl: ModalController,
private loadingCtrl: LoadingController,
private toastCtrl: ToastController,
private platform: Platform,
) {
platform.ready()
.then(() => {
console.log('Platform Ready');
});
}
ionViewDidLoad() {
this.newFileName = new Date().getTime().toString();
this.newFileNameM4A = this.newFileName +'.m4a';
}
onRecordAudio() {
this.media = new Media(this.newFileNameM4A);
this.media.startRecord();
this.isRecording = true;
}
onStopRecordAudio() {
this.media.stopRecord();
this.media.release();
this.isRecording = false;
this.isRecorded = true;
try {
File.copyFile(File.tempDirectory, this.newFileNameM4A, File.dataDirectory, this.newFileNameM4A)
.then(
(data: Entry) => {
this.audioUrl = data.nativeURL;
});
} catch (FileError) {
console.log(FileError)
};
}
onPlayback() {
this.media = new Media(this.newFileNameM4A);
this.media.play();
this.media.release();
}
onPlaybackTempDirectory() {
this.media = new Media(File.tempDirectory + this.newFileNameM4A);
this.media.play();
this.media.release();
}
onPlaybackDataDirectory() {
this.media = new Media(File.dataDirectory + this.localAudioUrl);
this.media.play();
this.media.release();
}
onHomerAudio() {
this.media = new Media(this.homerAudio)
this.media.play();
this.media.release();
}
}
Believe I may have solved this with the answer from https://issues.apache.org/jira/browse/CB-7007