So I am building a android video call app using webrtc I have implemented every thing and tested the video call feature from my app it works like charm. But now I want to implement where the user can switch camera from rear to front and vice verse. I have seen a similar answer it was not for android it said to remove the stream and negotiate and new stream I didnt properly understand how to implement that in this code so I hope someone can help me with that thing
Thank you
JavaScript File
localVideo.style.opacity = 0
remoteVideo.style.opacity = 0
localVideo.onplaying = () => { localVideo.style.opacity = 1 }
remoteVideo.onplaying = () => { remoteVideo.style.opacity = 1 }
let peer
function init(userId) {
peer = new Peer(userId, {
host: 'messenger-by-margs-video-call.herokuapp.com',
secure: true
})
peer.on('open', () => {
Android.onPeerConnected()
})
listen()
}
let localStream
function listen() {
peer.on('call', (call) => {
navigator.getUserMedia({
audio: true,
video: true
}, (stream) => {
localVideo.srcObject = stream
localStream = stream
call.answer(stream)
call.on('stream', (remoteStream) => {
remoteVideo.srcObject = remoteStream
remoteVideo.className = "primary-video"
localVideo.className = "secondary-video"
})
})
})
}
function startCall(otherUserId) {
navigator.getUserMedia({
audio: true,
video: true
}, (stream) => {
localVideo.srcObject = stream
localStream = stream
const call = peer.call(otherUserId, stream)
call.on('stream', (remoteStream) => {
remoteVideo.srcObject = remoteStream
remoteVideo.className = "primary-video"
localVideo.className = "secondary-video"
})
})
}
function toggleVideo(b) {
if (b == "true") {
localStream.getVideoTracks()[0].enabled = true
} else {
localStream.getVideoTracks()[0].enabled = false
}
}
function toggleAudio(b) {
if (b == "true") {
localStream.getAudioTracks()[0].enabled = true
} else {
localStream.getAudioTracks()[0].enabled = false
}
}
Heres my kotlin activity file.
class CallActivity : AppCompatActivity() {
var username = ""
var friendsUsername = ""
var isPeerConnected = false
var firebaseRef = FirebaseDatabase.getInstance().getReference("Users")
var isAudio = true
var isVideo = true
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_video_call)
username = FirebaseAuth.getInstance().uid.toString();
friendsUsername = intent.getStringExtra("userid")!!
call.setOnClickListener {
sendCallRequest()
}
toggleAudioBtn.setOnClickListener {
isAudio = !isAudio
callJavascriptFunction("javascript:toggleAudio(\"${isAudio}\")")
toggleAudioBtn.setImageResource(if (isAudio) R.drawable.ic_baseline_mic_24 else R.drawable.ic_baseline_mic_off_24 )
}
toggleVideoBtn.setOnClickListener {
isVideo = !isVideo
callJavascriptFunction("javascript:toggleVideo(\"${isVideo}\")")
toggleVideoBtn.setImageResource(if (isVideo) R.drawable.ic_baseline_videocam_24 else R.drawable.ic_baseline_videocam_off_24 )
}
setupWebView()
}
private fun sendCallRequest() {
if (!isPeerConnected) {
Toast.makeText(this, "You're not connected. Check your internet", Toast.LENGTH_LONG).show()
return
}
firebaseRef.child(friendsUsername).child("videocall").child("incall").setValue(username)
firebaseRef.child(friendsUsername).child("videocall").child("isAvailable").addValueEventListener(object: ValueEventListener {
override fun onCancelled(error: DatabaseError) {}
override fun onDataChange(snapshot: DataSnapshot) {
if (snapshot.value.toString() == "true") {
listenForConnId()
}
}
})
}
private fun listenForConnId() {
firebaseRef.child(friendsUsername).child("videocall").child("connId").addValueEventListener(object: ValueEventListener {
override fun onCancelled(error: DatabaseError) {}
override fun onDataChange(snapshot: DataSnapshot) {
if (snapshot.value == null)
return
switchToControls()
callJavascriptFunction("javascript:startCall(\"${snapshot.value}\")")
}
})
}
#SuppressLint("SetJavaScriptEnabled")
private fun setupWebView() {
webView.webChromeClient = object: WebChromeClient() {
override fun onPermissionRequest(request: PermissionRequest?) {
request?.grant(request.resources)
}
}
webView.settings.javaScriptEnabled = true
webView.settings.mediaPlaybackRequiresUserGesture = false
webView.addJavascriptInterface(JavascriptInterface(this), "Android")
loadVideoCall()
}
private fun loadVideoCall() {
val filePath = "file:android_asset/call.html"
webView.loadUrl(filePath)
webView.webViewClient = object: WebViewClient() {
override fun onPageFinished(view: WebView?, url: String?) {
initializePeer()
}
}
}
var uniqueId = ""
private fun initializePeer() {
uniqueId = getUniqueID()
callJavascriptFunction("javascript:init(\"${uniqueId}\")")
firebaseRef.child(username).child("videocall").child("incall").addValueEventListener(object: ValueEventListener {
override fun onCancelled(error: DatabaseError) {}
override fun onDataChange(snapshot: DataSnapshot) {
onCallRequest(snapshot.value as? String)
}
})
}
private fun onCallRequest(caller: String?) {
if (caller == null) return
callLayout.visibility = View.VISIBLE
incomingCallTxt.text = "$caller is calling..."
acceptBtn.setOnClickListener {
firebaseRef.child(username).child("videocall").child("connId").setValue(uniqueId)
firebaseRef.child(username).child("videocall").child("isAvailable").setValue(true)
callLayout.visibility = View.GONE
switchToControls()
}
rejectBtn.setOnClickListener {
firebaseRef.child(username).child("videocall").child("incall").setValue(null)
callLayout.visibility = View.GONE
}
}
private fun switchToControls() {
//inputLayout.visibility = View.GONE
call.visibility = View.INVISIBLE
callControlLayout.visibility = View.VISIBLE
}
private fun getUniqueID(): String {
return UUID.randomUUID().toString()
}
private fun callJavascriptFunction(functionString: String) {
webView.post { webView.evaluateJavascript(functionString, null) }
}
fun onPeerConnected() {
isPeerConnected = true
}
}
I hope these are necessary information to my answer
Related
I'm currently working on a webrtc project that was written before from another developer.
While I'm currently new to webrtc and I tried hard to make it work after deleting obsolete functions and other things, now can I identify what is wrong here in my steps?
I'm following steps in here
my code here is about trigger .call button
$(dod)
.find(".call")
and I run throw signaling
wbsc.emit("SEND_EVENT_EMIT_CALL_AUDIO", {
data: { type: "login", id: id },
});
$(dod).hide();
//call*donecallProccess 1
setTimeout(() => {
wbsc.emit("SEND_EVENT_EMIT_CALL_AUDIO", {
data: { type: "doneoif", id: id },
});
}, 2e3);
that can trigger and process here
case "donecall":
call(data.id);
break;
case "showcall":
handleLogin(data.success, data.id);
break;
case "offercall":
handleOffer(data.offer, data.name);
break;
case "answercall":
handleAnswer(data.answer);
break;
case "candidatecall":
handleCandidate(data.candidate);
break;
case "leavecall":
handleLeave();
then this code run one after each handle login and getUserMediaSuccess
gather permission from media stream and create a new RTCPeerConnection(servers)
get tracks from my streams with addtrack if I implement it correctly
and if ontrack happened can I collect streaming to add to remote peer like this way or should just add
yourConn.ontrack = (event) => {
if (event.candidate !== null) {
remoteVideo.srcObject = event.streams[0];
} else {
console.log("there is an error with on trackevent", event);
}
};
complete code for previous handlelogin and call is
let handleLogin = async (success) => {
try {
if (success) {
localVideo = document.getElementById("wbrtclocal");
remoteVideo = document.getElementById("wbrtcremote");
var getUserMedia = navigator.mediaDevices.getUserMedia|| navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia ;
if (getUserMedia) {
getUserMedia({
audio: { noiseSuppression: false },
})
.then(getUserMediaSuccess)
.catch(errorHandler);
} else {
alert("Your browser does not support getUserMedia API");
}
} else {
alert("Ooops...try a different username");
}
} catch (err) {
errorHandler(error);
}
};
let getUserMediaSuccess = async (streams) => {
try {
yourConn = new RTCPeerConnection(servers);
if(streams){
localStream = streams;
localVideo.srcObject = streams;
streams.getTracks().forEach((track) => yourConn.addTrack(track, streams));
}
yourConn.onicecandidate = (event) => {
if (event.candidate) {
wbsc.emit("SEND_EVENT_EMIT_CALL_AUDIO", {
data: {
type: "candidate",
candidate: event.candidate,
id: connectedUser,
},
});
}
console.log("nwe ice candidate is", event.candidate);
console.log("nwe stream is", streams);
};
remoteStream = new MediaStream();
remoteVideo.srcObject = remoteStream;
yourConn.ontrack = (event) => {
if (event.candidate !== null) {
event.streams[0].getTracks().forEach((track) => {
remoteStream.addTrack(track);
});
} else {
console.log("there is an error with on trackevent", event);
}
};
} catch (err) {
errorHandler(error);
}
console.log("stream is", streams);
};
call function
async function call(id) {
$("#videoCall").show();
if (id.length > 0) {
connectedUser = id;
try {
RTCPeerConnection.createOffer().
offer.
await yourConn
.createOffer()
.then((offer) => successCallback)
.catch((e) => {
fl(e);
});
let successCallback = async (offer) => {
try {
yourConn
.setLocalDescription(offer)
.then(
wbsc.emit("SEND_EVENT_EMIT_CALL_AUDIO", {
data: { type: "offer", offer: offer, id: connectedUser },
})
)
.catch((e) => {
fl(e);
});
} catch (e) {
fl(e);
}
};
} catch (e) {
fl(e);
}
const user = U_CASH[id];
if (user) {
$("#videoCall")
.find(".u-pic")
.css("background-image", "url(" + removegifpic(user.pic + ")"));
$("#videoCall").find(".u-topic").text(user.topic);
}
$(".statecall").text("جاري الإتصال");
hl($(".statecall"), "warning");
} else {
alert("username can't be blank!");
}
console.log("connectedUser", id);
console.log("offer", offer);
console.log();
console.log();
console.log();
}
and this about each one for those
[handleOffer , handleAnswer , handleCandidate , handleLeave]
let handleOffer = async (offer, name) => {
$("#callvideonot").show();
const user = U_CASH[name];
if (user) {
$("#callvideonot")
.find(".u-pic")
.css("background-image", "url(" + removegifpic(user.pic + ")"));
$("#callvideonot").find(".u-topic").text(user.topic);
}
$(".callvideoaccept").on("click", async () => {
connectedUser = name;
await yourConn.setRemoteDescription(offer);
yourConn
.createAnswer()
.then((answer) => t.setLocalDescription(answer))
.then(() => {
wbsc.emit("SEND_EVENT_EMIT_CALL_AUDIO", {
data: { type: "answer", answer: answer, id: connectedUser },
});
})
.catch(fl);
const user = U_CASH[name];
if (user) {
$(".statecall").text("متصل");
hl($(".statecall"), "success");
$("#videoCall")
.find(".u-pic")
.css("background-image", "url(" + removegifpic(user.pic + ")"));
$("#videoCall").find(".u-topic").text(user.topic);
}
$("#callvideonot").hide();
$("#videoCall").show();
});
$(".callvideodeny").on("click", function () {
wbsc.emit("SEND_EVENT_EMIT_CALL_AUDIO", {
data: { type: "leave", id: name },
});
});
};
let handleAnswer = async (answer) => {
try {
$(".statecall").text("متصل");
hl($(".statecall"), "success");
//here we delete new RTCSessionDescription because constructor is deprecated.
await yourConn.setRemoteDescription(answer);
} catch (e) {
fl(e);
}
};
let handleCandidate = async (candidate) => {
try {
var NewlyIceCandidate = new RTCIceCandidate(candidate)
.setRemoteDescription().
await yourConn.addIceCandidate(NewlyIceCandidate);
} catch (e) {
fl(e);
}
};
function handleLeave() {
$("#callvideonot").hide();
$(".statecall").text("رفض");
hl($(".statecall"), "danger");
$(".vloumemic").removeClass("fa-volume-off");
$(".vloumemic").addClass("fa-volume-up");
$(".mutemic").removeClass("fa-microphone-slash");
$(".mutemic").addClass("fa-microphone");
setTimeout(() => {
$("#videoCall").hide();
}, 1e3);
if (localStream) {
localStream.getTracks().forEach((e) => e.stop());
}
if (connectedUser) {
connectedUser = null;
}
remoteVideo.src = null;
if (yourConn) {
yourConn.close();
yourConn.onicecandidate = null;
yourConn.ontrack = null;
localStream = null;
}
}
in here here number 7 instruction they said should I add Wait for an incoming remote SDP description from the signaling service and set it using RTCPeerConnection.setRemoteDescription(). as the caller where can I add it? in handleCandidate function?
and in the callee side number 1 instruction the said that I should
Create a new RTCPeerConnection instance with the appropriate ICE configuration.
can I reuse
yourConn = new RTCPeerConnection(servers);
or should instantiate a new one to prevent conflict in website server process
as a not yourConn it's global value and in top level of this file and reuse it over all
the connection, is peers 2 or the callee should have another new RTCPeerConnection?
and for remoteVideo.srcObject = remoteStream; the remoteStream value is global and I overwrite it. can I here add new media stream or just it's good to dealing with the present one which is remoteVideo element?
// remoteStream = new MediaStream();
remoteVideo.srcObject = remoteStream;
the issue that was here is to split the RTCPeerConnection object and just create new one for each peer local and remote that what i did and it working corectly after i remove addtrack completly and replace it with addtranceiver and gettranceiver
u can find it here from previous issue's answer and i fix it here
and no need to overwrite or reset the srcObect value. The track transition on receiver side in the same MediaStream should be "seamless" RTCRtpSender.replaceTrack
This allows you to seamlessly change which track is being sent without having to renegotiate at the expense of another offer/answer cycle
As per our web RTC requirements, there are two different client
Player (Players the screen shared by capture client)
Capture (Share Screen)
The two web clients communicate and exchange offers and ICE candidates using WebSocket.
In Chrome [Version 84.0.4147.105 (Official Build) (64-bit)]
There is no error in the Player and Capture javascript console in chrome.
But if we check chrome://webrtc-internals/ we can see the following event and transmission graph:
Player
Capture
Here the I can see the video streaming is transmission but not playing in payer end and an ICE Candidate error in showing up int he events log. Is that is the problem the video stream is not working in the payer end?
Firefox (v79.0)
Showing errors in the console:
DOMException: No remoteDescription.
In player.js line no: 33.
Any Idea why two different browsers have different errors?
Player.js
(function(){
var localVideo, remoteVideo, localConnection, remoteConnection;
const MESSAGE_TYPE = {
SDP: 'SDP',
CANDIDATE_LOCAL: 'LOCAL_CANDIDATE',
CANDIDATE_REMOTE: 'REMOTE_CANDIDATE'
};
const signaling = new WebSocket('ws://127.0.0.1:1337');
var configuration = {
offerToReceiveAudio: true,
offerToReceiveVideo: true
}
remoteConnection = new RTCPeerConnection({configuration: configuration, iceServers: [{ urls: 'stun:aalimoshaver.com:3478' }]});
remoteConnection.onicecandidate = function(e) { !e.candidate
|| signaling.send(JSON.stringify({message_type:MESSAGE_TYPE.CANDIDATE_REMOTE, content: e.candidate.toJSON()}));
}
remoteConnection.ontrack = function (e) {
const remoteVideo = document.getElementById('remote-view');
if (!remoteVideo.srcObject) {
remoteVideo.srcObject = e.streams[0];
}
};
signaling.onmessage = function (message){
const data = JSON.parse(message.data);
const message_type = data.message_type;
const content = data.content;
try {
if (message_type === MESSAGE_TYPE.CANDIDATE_LOCAL && content) {
remoteConnection.addIceCandidate(content)
.catch(function (e) {
console.error(e)
});
}else if (message_type === MESSAGE_TYPE.SDP && content) {
if (content.type === 'offer') {
remoteConnection.setRemoteDescription(content);
remoteConnection.createAnswer()
.then(function(answer){
remoteConnection.setLocalDescription(answer);
signaling.send(JSON.stringify({
message_type: MESSAGE_TYPE.SDP,
content: answer
}));
});
} else {
console.log('Unsupported SDP type.');
}
}
} catch (err) {
console.error(err);
}
};
})()
Capture.js
/**
* Created by Sowvik Roy on 30-07-2020.
*/
(function () {
var localVideo, remoteVideo, localConnection, remoteConnection;
const MESSAGE_TYPE = {
SDP_LOCAL: 'SDP',
CANDIDATE_LOCAL: 'LOCAL_CANDIDATE',
CANDIDATE_REMOTE: 'REMOTE_CANDIDATE'
};
var configuration = {
offerToReceiveAudio: true,
offerToReceiveVideo: true
};
const signaling = new WebSocket('ws://127.0.0.1:1337');
signaling.onmessage = function (message){
const data = JSON.parse(message.data);
const message_type = data.message_type;
const content = data.content;
try {
if (message_type === MESSAGE_TYPE.CANDIDATE_REMOTE && content) {
localConnection.addIceCandidate(content)
.catch(function (e) {
console.error(e)
});
} else if (message_type === MESSAGE_TYPE.SDP_LOCAL) {
if (content.type === 'answer') {
localConnection.setRemoteDescription(content);
} else {
console.log('Unsupported SDP type.');
}
}
} catch (err) {
console.error(err);
}
};
document.addEventListener('click', function (event) {
if (event.target.id === 'start') {
startChat();
localVideo = document.getElementById('self-view');
remoteVideo = document.getElementById('remote-view');
}
});
function startConnection(){
localConnection = new RTCPeerConnection({configuration: configuration, iceServers: [{ urls: 'stun:aalimoshaver.com:3478' }]});
localConnection.onicecandidate = function (e) {
!e.candidate
|| signaling.send(JSON.stringify({message_type:MESSAGE_TYPE.CANDIDATE_LOCAL, content: e.candidate.toJSON()}));
};
localConnection.createOffer()
.then(function (offer) {
if(offer){
localConnection.setLocalDescription(offer);
signaling.send(JSON.stringify({message_type:MESSAGE_TYPE.SDP_LOCAL, content: localConnection.localDescription}));
if (navigator.getDisplayMedia) {
navigator.getDisplayMedia({video: true}).then(onCaptureSuccess);
} else if (navigator.mediaDevices.getDisplayMedia) {
navigator.mediaDevices.getDisplayMedia({video: true}).then(onCaptureSuccess);
} else {
navigator.mediaDevices.getUserMedia({video: {mediaSource: 'screen'}}).then(onCaptureSuccess);
}
}
else{
console.error("RTC offer is null");
}
})
.catch(function (e) {
console.error(e)
});
}
function onCaptureSuccess(stream){
localVideo.srcObject = stream;
stream.getTracks().forEach(
function (track) {
localConnection.addTrack(
track,
stream
);
}
);
}
function startChat() {
if (navigator.getDisplayMedia) {
navigator.getDisplayMedia({video: true}).then(onMediaSuccess);
} else if (navigator.mediaDevices.getDisplayMedia) {
navigator.mediaDevices.getDisplayMedia({video: true}).then(onMediaSuccess);
} else {
navigator.mediaDevices.getUserMedia({video: {mediaSource: 'screen'}}).then(onMediaSuccess);
}
}
function onMediaSuccess(stream) {
localVideo.srcObject = stream;
// Set up the ICE candidates for the two peers
localConnection = new RTCPeerConnection({configuration: configuration, iceServers: [{ urls: 'stun:stun.xten.com:19302' }]});
localConnection.onicecandidate = function (e) {
!e.candidate
|| signaling.send(JSON.stringify({message_type:MESSAGE_TYPE.CANDIDATE_LOCAL, content: e.candidate.toJSON()}));
};
stream.getTracks().forEach(
function (track) {
localConnection.addTrack(
track,
stream
);
}
);
localConnection.createOffer()
.then(function (offer) {
if(offer){
localConnection.setLocalDescription(offer);
signaling.send(JSON.stringify({message_type:MESSAGE_TYPE.SDP_LOCAL, content: localConnection.localDescription}));
}
else{
console.error("RTC offer is null");
}
})
.catch(function (e) {
console.error(e)
});
}
})();
Can anybody explain or identify a loophole in the code? Please let me know if you need additional info.
when trying to cancel upload by unsubscribing what actually happen that i unsubscribe to upload progress but that actual upload is not cancelled and keep uploading to the server.
upload.components.ts
import { Component, OnInit, Input, Output, EventEmitter, OnDestroy } from '#angular/core';
import { Subject, Subscription, Observable } from 'rxjs';
import { HttpEventType } from '#angular/common/http';
import { UploadService } from '../../../services';
import { takeUntil } from 'rxjs/operators';
#Component({
selector: 'app-image-upload-item',
templateUrl: './image-upload-item.component.html',
styleUrls: ['./image-upload-item.component.scss']
})
export class ImageUploadItemComponent implements OnInit, OnDestroy {
#Input() index: any;
#Output() uploadSuccess: EventEmitter<any>;
#Output() uploadCanceled: EventEmitter<any>;
public localimageURL: string;
public uploadProgress: number;
public isUploadCompleted: boolean;
public uploadImageObservable: Subscription;
public isReadyForUpload: boolean;
public isUploading: boolean;
public progressMode: string;
public readonly unique: string = Math.floor((Math.random() *
100)).toString();
public readonly imagePreviewID = 'imagePreview' + this.unique;
_file: any;
#Input() public set file(value: any) {
const reader = new FileReader();
reader.onload = (e: any) => {
this.localimageURL = e.target.result;
};
this._file = value;
reader.readAsDataURL(this._file);
console.log(this._file);
}
constructor(private uploadService: UploadService) {
this.uploadProgress = 0;
this.isUploading = false;
this.localimageURL = '';
this.isUploadCompleted = false;
this.uploadSuccess = new EventEmitter<any>();
this.uploadCanceled = new EventEmitter<any>();
this.progressMode = 'indeterminate';
}
ngOnInit() {
this.uploadImageToServer(this._file);
// setTimeout(() => {
// console.log('im in set time out unsubscripting',
this.uploadImageObservable);
// this.uploadImageObservable.forEach(subscription => {
// subscription.unsubscribe();
// });
// }, 100);
}
ngOnDestroy() {
console.log('component destroyed');
this.uploadImageObservable.unsubscribe();
}
public clearUploadButtonClicked() {
// if (this.uploadImageObservable !== undefined) {
// console.log('image observable is defined');
// this.uploadImageObservable.unsubscribe();
// console.log(this.uploadImageObservable.closed);
// }
// this.uploadImageObservable.unsubscribe();
this._file = '';
this.uploadCanceled.emit({ index: this.index, uploaded: false });
}
public get showUploadProgress(): boolean {
return this.uploadProgress !== 0;
}
public uploadImageToServer(file) {
this.isUploading = true;
const progress = new Subject<number>();
progress.subscribe(value => {
this.uploadProgress = value;
});
this.uploadImageObservable = this.uploadService.uploadImage(file)
.subscribe(result => {
const type = result.type;
const data = result.data;
console.log(result);
if (type === HttpEventType.UploadProgress) {
const percentDone = Math.round(100 * data.loaded / data.total);
progress.next(percentDone);
if (percentDone === 100) {
this.progressMode = 'indeterminate';
}
} else if (type === HttpEventType.Response) {
if (data) {
progress.complete();
this.progressMode = 'determinate';
this.isReadyForUpload = false;
this.isUploadCompleted = true;
this.isUploading = false;
this.uploadSuccess.emit({ index: this.index, mediaItem: data });
}
}
}, errorEvent => {
});
}
}
upload.service.ts
public uploadImage(imageFile: File): Observable<any> {
const formData: FormData = new FormData();
if (imageFile !== undefined) {
formData.append('image', imageFile, imageFile.name);
const req = new HttpRequest('POST', environment.uploadImageEndPoint,
formData, {
reportProgress: true,
});
return new Observable<any>(observer => {
this.httpClient.request<any>(req).subscribe(event => {
if (event.type === HttpEventType.Response) {
const responseBody = event.body;
if (responseBody) {
this.alertService.success(responseBody.message);
observer.next({ type: event.type, data: new
MediaItem(responseBody.mediaItem) });
}
} else if (event.type === HttpEventType.UploadProgress) {
observer.next({ type: event.type, data: { loaded: event.loaded, total:
event.total } });
} else {
observer.next(event);
}
}, errorEvent => {
if (errorEvent.status === 400) {
this.alertService.error(errorEvent.error['image']);
} else {
this.alertService.error('Server Error, Please try again later!');
}
observer.next(null);
});
});
}
}
how can i cancel upload request properly with observable unsubscribe
note i already tried pipe takeuntil() and nothing changed
What you'll want to do is return the result from the pipe function on the http request return observable. Right now you have multiple streams and the component's unsubscribe is only unsubscribing to the observable wrapping the http request observable (not connected).
You'll want to do something like:
return this.httpClient.request<any>(req).pipe(
// use rxjs operators here
);
You'll then use rxjs operators (I've been doing this for a while, but I still highly reference this site) to perform any logic needed and reflect things like your errors and upload progress to the component calling the service. On the component side, you'll keep your subscribe/unsubscribe logic.
For instance, you can use the switchMap operator to transform what is returning to the component from the http request observable and specify the value to return to the component, and catchError to react to any errors accordingly.
return this.httpClient.request<any>(req).pipe(
switchMap(event => {
if (event.type === HttpEventType.Response) {
const responseBody = event.body;
if (responseBody) {
this.alertService.success(responseBody.message);
return { type: event.type, data: new MediaItem(responseBody.mediaItem) };
}
} else if (event.type === HttpEventType.UploadProgress) {
return { type: event.type, data: { loaded: event.loaded, total: event.total } };
}
return event;
}),
catchError(errorEvent => {
if (errorEvent.status === 400) {
this.alertService.error(errorEvent.error['image']);
} else {
this.alertService.error('Server Error, Please try again later!');
}
return of(<falsy or error value>);
}),
);
Alternatively you could model it a little more after this example by just returning the http function call from the service to the component and handling things in the subscribe there.
actually i found a way as follows
public uploadImage(imageFile: File): Observable<any> {
const formData: FormData = new FormData();
if (imageFile !== undefined) {
formData.append('image', imageFile, imageFile.name);
const req = new HttpRequest('POST', environment.uploadImageEndPoint, formData, {
reportProgress: true,
});
return this.httpClient.request<any>(req).pipe(
map((res: any) => {
return res;
}),
catchError(errorEvent => {
if (errorEvent.status === 400) {
this.alertService.error(errorEvent.error['image']);
} else {
this.alertService.error('Server Error, Please try again later!');
return Observable.throw(errorEvent);
}
return Observable.throw(errorEvent);
}));
}
}
For this challenge, you are going to build a mock comments section.
Design
We're going to focus on two aspects:
Users
Users come in 3 flavors, normal users, moderators, and admins. Normal users can only create new comments, and edit the their own
comments. Moderators have the added ability to delete comments (to
remove trolls), while admins have the ability to edit or delete any
comment.
Users can log in and out, and we track when they last logged in
Comments
Comments are simply a message, a timestamp, and the author.
Comments can also be a reply, so we'll store what the parent comment was.
Beneath is my code:
class Admin extends Moderator {
constructor(name) {
super(name);
}
canEdit(comment) {
return true;
}
}
class Comment {
constructor(author, message, repliedTo) {
this.createdAt = new Date();
this._author = author;
this._message = message;
this.repliedTo = repliedTo || null;
}
getMessage() {
return this._message;
}
setMessage(message) {
this._message = message;
}
getCreatedAt() {
return this.createdAt;
}
getAuthor() {
return this._author;
}
getRepliedTo() {
return this.repliedTo;
}
getString(comment) {
const authorName = comment.getAuthor().getName();
if (!comment.getRepliedTo()) return authorName;
return `${comment.getMessage()} by ${authorName} (replied to ${this.getString(comment.getRepliedTo())})`;
}
toString() {
const authorName = this.getAuthor().getName();
if (!this.getRepliedTo()) {
return `${this._message} by ${authorName}`;
}
return this.getString(this);
}
}
I get the error
The toString method should return the correct hierarchy (nested reply)
Although this supposed to be an assignment, the question was a bit technical and unclear; this's the proven solution
class User {
constructor(name) {
this._name = name;
this._loggedIn = false;
this._lastLoggedInAt = null;
}
isLoggedIn() {
return this._loggedIn;
}
getLastLoggedInAt() {
return this._lastLoggedInAt;
}
logIn() {
this._lastLoggedInAt = new Date();
this._loggedIn = true;
}
logOut() {
this._loggedIn = false
}
getName() {
return this._name;
}
setName(name) {
this._name = name;
}
canEdit(comment) {
if(comment._author._name === this._name) {
return true;
}
return false;
}
canDelete(comment) {
return false;
}
}
class Moderator extends User {
constructor(name) {
super(name);
}
canDelete(comment) {
return true;
}
}
class Admin extends Moderator {
constructor(name) {
super(name)
}
canEdit(comment) {
return true;
}
}
class Comment {
constructor(author = null, message, repliedTo = null) {
this._createdAt = new Date();
this._message = message;
this._repliedTo = repliedTo;
this._author = author;
}
getMessage() {
return this._message;
}
setMessage(message) {
this._message = message;
}
getCreatedAt() {
return this._createdAt;
}
getAuthor() {
return this._author;
}
getRepliedTo() {
return this._repliedTo;
}
toString() {
if(this._repliedTo === null) {
return this._message + " by " + this._author._name
}
return this._message + " by " + this._author._name + " (replied to " +
this._repliedTo._author._name + ")"
}
}
The error was because you were calling a getName() method on getAuthor method which wasn't available. You can get author name directly from Comment this._author._name.
I make use of JavaScript constructor coding style, to write this solution but it should not matter much as the solution does not need you to change your style. Observe that the fields (_author, _message, _repliedTo) are private, and private fields can only be accessed through public methods. And that is basically what I did here in the toString() method.
function Comment(author, message, repliedTo = null) {
var _author = author;
var _message = message;
var _repliedTo = repliedTo;
this.getAuthor = function() {
return _author;
};
this.getRepliedTo = function() {
return _repliedTo;
};
this.toString = function() {
return ((_repliedTo === null) ? message + " by " + _author.getName() : message + " by " + _author.getName() + " (replied to " + this.getRepliedTo().getAuthor().getName() + ")");
}
};
You can remove the getString() method...
toString()
{
return ((this._repliedTo === null) ? this._message + " by " +
this._author.getName() : this._message + " by " +
this._author.getName() + " (replied to " + this._repliedTo._author.getName() + ")");
}
class User {
function __construct($name) {
private $name;
private $loggedIn;
private $lastLoggedInAt;
$this->name = $name;
$this->loggedIn = false;
$this->lastLoggedInAt = null;
}
function isLoggedIn() {
return $this->loggedIn;
}
function getLastLoggedInAt() {
return $this->lastLoggedInAt;
}
function logIn() {
$this->lastLoggedInAt = new Date('Y-m-d H:i:s');
$this->loggedIn = true;
}
function logOut() {
$this->loggedIn = false;
}
function getName() {
return $this->name;
}
function setName($name) {
$this->name = $name;
}
function canEdit($comment) {
if($comment->author->name === $this->name) {
return true;
}
return false;
}
function canDelete($comment) {
return false;
}
}
class Moderator extends User {
function __construct($name) {
$this->name = $name;
}
function canDelete($comment) {
return true;
}
}
class Admin extends Moderator {
function constructor($name) {
$this->name = $name;
}
function canEdit($comment) {
return true;
}
}
class Comment {
function __construct($author = null, $message, $repliedTo = null) {
private $createdAt;
private $message;
private $repliedTo;
private $author;
$this->createdAt = new Date('Y-m-d H:i:s');
$this->message = $message;
$this->repliedTo = $repliedTo;
$this->author = $author;
}
function getMessage() {
return $this->message;
}
function setMessage($message) {
$this->message = $message;
}
function getCreatedAt() {
return $this->createdAt;
}
function getAuthor() {
return $this->author;
}
function getRepliedTo() {
return $this->repliedTo;
}
function __toString() {
if($this->repliedTo === null) {
return $this->message + " by " + $this->author->name;
}
return $this->message + " by " + $this->author->name + " (replied to " +
$this->repliedTo->author->name + ")";
}
}
Java version if anyone needs:
import java.util.Date;
public class Solution {
public static class User {
String name;
boolean loggedIn;
Date lastLoggedInAt;
public User(String name) {
this.name = name;
this.loggedIn = loggedIn;
this.lastLoggedInAt = lastLoggedInAt;
}
public boolean isLoggedIn() {
return this.loggedIn;
}
public Date getLastLoggedInAt() {
return this.lastLoggedInAt;
}
public void logIn() {
this.lastLoggedInAt = new Date();
this.loggedIn = true;
}
public void logOut() {
this.loggedIn = false;
}
public String getName() {
return this.name;
}
public void setName(String name) {
this.name = name;
}
public boolean canEdit(Comment comment) {
if(comment.getAuthor().name == this.name) {
return true;
}
return false;
}
public boolean canDelete(Comment comment) {
return false;
}
}
public static class Moderator extends User{
public Moderator(String name) {
super(name);
}
public boolean canDelete(Comment comment) {
return true;
}
}
public static class Admin extends Moderator{
public Admin(String name) {
super(name);
}
public boolean canEdit(Comment comment) {
return true;
}
}
public static class Comment {
User author;
//注意下面也要是author
String message;
Comment comment;
Date createdAt;
Comment repliedTo;
public Comment(User author, String message) {
this.author = author;
this.message = message;
}
public Comment(User author, String message, Comment repliedTo) {
this.author = author;
this.message = message;
this.repliedTo = repliedTo;
}
public String getMessage() {
return this.message;
}
public void setMessage(String message) {
this.message = message;
}
public Date getCreatedAt() {
return this.createdAt;
}
public User getAuthor() {
return this.author;
}
public Comment getRepliedTo() {
return this.repliedTo;
}
public String toString() {
if(this.repliedTo == null) {
return this.message + " by " + this.author.getName();
}
return this.message + " by " + this.author.getName() + " (replied to " +
this.repliedTo.getAuthor().name + ")";
}
}
}
Answers provided above won't pass basic unit tests for this assignment. Here's an option I successfully submitted to complete the challenge:
export class User {
constructor(name) {
this._name = name;
this._lastLoginDate = null;
this._loggedIn = false;
}
isLoggedIn() {
return this._loggedIn;
}
getLastLoggedInAt() {
return this._lastLoginDate;
}
logIn() {
this._lastLoginDate = new Date();
return Promise.resolve('Success').then(() => {
this._loggedIn = true;
});
}
logOut() {
this._loggedIn = false;
}
getName() {
return this._name;
}
setName(name) {
this._name = name;
}
canEdit(comment) {
if (comment.getAuthor().getName() === this.getName()) {
return true;
}
return false;
}
canDelete(comment) {
return false;
}
}
export class Moderator extends User {
constructor(name) {
super(name);
}
canDelete(comment) {
return true;
}
}
export class Admin extends Moderator {
constructor(name) {
super(name);
}
canEdit(comment) {
return true;
}
}
export class Comment {
constructor(author, message, repliedTo = null) {
this._author = author;
this._message = message;
this._repliedTo = repliedTo || null;
this._createdAt = new Date();
}
getMessage() {
return this._message;
}
setMessage(message) {
this._message = message;
}
getCreatedAt() {
return this._createdAt;
}
getAuthor() {
return this._author;
}
getRepliedTo() {
return this._repliedTo;
}
toString() {
return this.getRepliedTo() === null
? `${this.getMessage()} by ${this.getAuthor().getName()}`
: `${this.getMessage()} by ${this.getAuthor().getName()} (replied to ${this.getRepliedTo()
.getAuthor()
.getName()})`;
}
}
If you want to solve this thing using typescript:
export class User {
private _name: string;
private _loggedIn: boolean;
private _lastLoggedInAt: Date | null;
constructor(name: string) {
this._name = name;
this._loggedIn = false;
this._lastLoggedInAt = null;
}
isLoggedIn(): boolean {
return this._loggedIn;
}
getLastLoggedInAt(): Date | null {
return this._lastLoggedInAt;
}
async logIn(): Promise<void> {
this._lastLoggedInAt = new Date();
await Promise.resolve("suceess");
this._loggedIn = true;
}
logOut(): void {
this._loggedIn = false;
}
getName(): string {
return this._name;
}
setName(name: string): void {
this._name = name;
}
canEdit(comment: Comment): boolean {
if (comment.getAuthor().getName() === this._name) {
return true;
}
return false;
}
canDelete(comment: Comment): boolean {
return false;
}
}
export class Moderator extends User {
constructor(name: string) {
super(name);
}
canDelete(_comment: Comment): boolean {
return true;
}
}
export class Admin extends Moderator {
constructor(name: string) {
super(name);
}
canEdit(_comment: Comment): boolean {
return true;
}
}
export class Comment {
private _author: User;
private _message: string;
private _repliedTo?: Comment | null;
private _createdAt: Date;
constructor(author: User, message: string, repliedTo?: Comment) {
this._author = author;
this._message = message;
this._repliedTo = repliedTo;
this._createdAt = new Date();
}
getMessage(): string {
return this._message;
}
setMessage(message: string): void {
this._message = message;
}
getCreatedAt(): Date {
return this._createdAt;
}
getAuthor(): User {
return this._author;
}
getRepliedTo(): Comment | null {
if (this._repliedTo) {
return this._repliedTo;
}
return null;
}
toString(): string {
if (this.getRepliedTo()) {
return `${this.getMessage()} by ${this.getAuthor().getName()} (replied to ${this._repliedTo
?.getAuthor()
.getName()})`;
}
return `${this.getMessage()} by ${this.getAuthor().getName()}`;
}
}
and few unit tests using Jest
describe('Normal user tests', function() {
it('should return name of the User', () => {
const user = new User("User 1");
expect(user.getName()).toEqual('User 1');
});
it('shoud return isLoggedIn as true when logIn method is called' , () => {
const user = new User("User 1");
user.logIn().then(() => {
expect(user.isLoggedIn()).toBeTruthy();
}).catch((error) => {
expect(user.isLoggedIn()).toBe(false);
});;
});
it('shoud return _lastLoggedInAt as the date when logIn method is called' , () => {
const user = new User("User 1");
user.logIn().then(() => {
expect(user.getLastLoggedInAt()).toBe(new Date());
})
});
it('shoud return _loggedIn as false when logOut method is called', () => {
const user = new User("User 1");
user.logOut();
expect(user.isLoggedIn()).toBe(false);
});
it('shoud setName of the user' , () => {
const user = new User("User");
user.setName("User 2");
expect(user.getName()).toEqual('User 2');
});
});
describe('Moderator user tests', function() {
it('should return name of the User', () => {
const moderator = new Moderator("Moderator 1");
const message = "Hello there"
const comment = new Comment (moderator, message);
expect(moderator.canDelete(comment)).toBeTruthy();
});
});
describe('Admin user tests', function() {
it('should return name of the User', () => {
const admin = new Admin("Admin 1");
const message = "Hello there"
const comment = new Comment (admin, message);
expect(admin.canEdit(comment)).toBeTruthy();
});
});
describe('Comment tests', function() {
it('should return message of the author', () => {
const user = new User("User 1");
const message = "Hi! This is my message."
const comment = new Comment (user, message)
expect(comment.getMessage()).toEqual(message);
});
it('should set new message', () => {
const user = new User("User 1");
const message = "Hi! This is my message."
const newMessage = "Hi! This is new message."
const comment = new Comment (user, message);
comment.setMessage(newMessage)
expect(comment.getMessage()).toEqual(newMessage);
});
it('should return null when replied to does not exists', () => {
const user = new User("User 1");
const message = "Hi! This is my message."
const comment = new Comment (user, message);
expect(comment.getRepliedTo()).toBe(null);
});
it('should return repliedTo when replied to exists', () => {
const user = new User("User 1");
const message = "Hi! This is my message."
const repliedToUser = new User("User 2");
const repliedToMessage = "Hi! This is my replied message.";
const repliedToComment = new Comment (repliedToUser, repliedToMessage);
const comment = new Comment (user, message, repliedToComment);
expect(comment.getRepliedTo()).toBe(repliedToComment);
});
it('should return repliedTo string message when replied to exists', () => {
const user = new User("User1");
const message = "Hello there"
const repliedToUser = new User("User2");
const repliedToMessage = "Hi! This is my replied message.";
const repliedToComment = new Comment (repliedToUser, repliedToMessage);
const comment = new Comment (user, message, repliedToComment);
expect(comment.toString()).toBe("Hello there by User1 (replied to User2)");
});
it('should return comment string message when replied to does not exists', () => {
const user = new User("User1");
const message = "Hello there"
const comment = new Comment (user, message);
expect(comment.toString()).toBe("Hello there by User1");
});
});
I have a Sitefinity app hosted in Azure CloudService and I use signalr to push data to clients. Recently monitoring the app with new relic I found that the signalr its on top of error rate with this request: [host]/signalr/connect/ as you may see in the image. Also say that push work perfectly.
capture of error rate in newrelic
My Server side:
[assembly: OwinStartup(typeof(Startup))]
namespace SitefinityWebApp.FXS.Custom.AppStart
{
public class Startup
{
public void Configuration(IAppBuilder app)
{
GlobalHost.HubPipeline.AddModule(new SignalRErrorHandler());
var hubConfiguration = new HubConfiguration
{
EnableDetailedErrors = true
};
app.MapSignalR(hubConfiguration);
}
}
}
namespace SitefinityWebApp.Hubs.Posts
{
public class PostHub : Hub
{
}
public class PostNotification : IPostNotification
{
private readonly static Lazy<PostNotification> instance = new Lazy<PostNotification>(() => new PostNotification());
private readonly IHubContext iHubContext;
private PostNotification()
: this(GlobalHost.ConnectionManager.GetHubContext<PostHub>())
{
}
internal PostNotification(IHubContext iHubContext)
{
this.iHubContext = Guard.ArgumentNotNullAndReturn(iHubContext, "iHubContext");
}
public static PostNotification Instance
{
get
{
return instance.Value;
}
}
public void PostCreated(string action, PostResponse post)
{
var proxy = this.iHubContext.Clients.All;
proxy.Invoke(action, post);
}
}
}
My client side:
(function () {
Class.PostNotifications = function () {
var parent = Class.Base(),
_this = Util.extendObject(parent);
_this.ReconnectionTime = 5000;
_this.PostCreated_ObserverSubject = null;
_this.PostHub = null;
_this.ContentType = "";
_this.constructor = function (contentType) {
_this.ContentType = contentType;
_this.startConnection();
_this.setVars();
_this.configurePostHub();
};
_this.setVars = function () {
_this.PostHub = $.connection.postHub;
_this.PostCreated_ObserverSubject = new Class.Patterns.Observer.Subject();
};
_this.configurePostHub = function () {
_this.PostHub.client[_this.ContentType + 'Created'] = function (post) {
_this.PostCreated_ObserverSubject.notify(post);
};
};
_this.whenPostCreated = function (functionDelegate) {
if (functionDelegate !== undefined && functionDelegate !== null) {
var json = {
'UpdateDelegate': functionDelegate
};
var observer = new Class.Patterns.Observer.Observer();
observer.init(json);
_this.PostCreated_ObserverSubject.addObserver(observer);
}
};
_this.startConnection = function () {
$.connection.hub.logging = true;
$.connection.hub.start().done(function () {
console.log("connection stablish");
}).fail(function (err) {
console.log(err);
});
$.connection.hub.disconnected(function () {
setTimeout(function () {
$.connection.hub.start();
}, _this.ReconnectionTime);
});
};
return _this;
};
}());
Any help would be appreciated