mirror of
https://github.com/VDR4Arch/vdr.git
synced 2023-10-10 13:36:52 +02:00
Adapted frame detection to driver 0.8+
This commit is contained in:
parent
05c61fe624
commit
7c79f61dd5
3
HISTORY
3
HISTORY
@ -320,7 +320,8 @@ Video Disk Recorder Revision History
|
|||||||
the DVB/driver directory.
|
the DVB/driver directory.
|
||||||
Old recordings (in AV_PES mode) can still be replayed (as long as the driver
|
Old recordings (in AV_PES mode) can still be replayed (as long as the driver
|
||||||
still supports replaying AV_PES files). The only limitation with this is that
|
still supports replaying AV_PES files). The only limitation with this is that
|
||||||
in fast forward/back mode the picture may be slightly distorted.
|
in fast forward/back mode the picture may be slightly distorted and there may
|
||||||
|
be sound fragments.
|
||||||
- The EPG data is now dumped into the file /video/epg.data every ten minutes.
|
- The EPG data is now dumped into the file /video/epg.data every ten minutes.
|
||||||
Use the Perl script 'epg2html.pl' to convert the raw EPG data into a simple
|
Use the Perl script 'epg2html.pl' to convert the raw EPG data into a simple
|
||||||
HTML programme listing.
|
HTML programme listing.
|
||||||
|
170
dvbapi.c
170
dvbapi.c
@ -4,7 +4,7 @@
|
|||||||
* See the main source file 'vdr.c' for copyright information and
|
* See the main source file 'vdr.c' for copyright information and
|
||||||
* how to reach the author.
|
* how to reach the author.
|
||||||
*
|
*
|
||||||
* $Id: dvbapi.c 1.44 2000/12/25 15:18:02 kls Exp $
|
* $Id: dvbapi.c 1.45 2001/01/07 17:00:50 kls Exp $
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#include "dvbapi.h"
|
#include "dvbapi.h"
|
||||||
@ -45,8 +45,10 @@ extern "C" {
|
|||||||
#define B_FRAME 3
|
#define B_FRAME 3
|
||||||
|
|
||||||
// Start codes:
|
// Start codes:
|
||||||
#define SC_PICTURE 0x00
|
#define SC_PICTURE 0x00 // "picture header"
|
||||||
#define SC_BLOCK 0xBA
|
#define SC_SEQU 0xB3 // "sequence header"
|
||||||
|
#define SC_PHEAD 0xBA // "pack header"
|
||||||
|
#define SC_SHEAD 0xBB // "system header"
|
||||||
#define SC_AUDIO 0xC0
|
#define SC_AUDIO 0xC0
|
||||||
#define SC_VIDEO 0xE0
|
#define SC_VIDEO 0xE0
|
||||||
|
|
||||||
@ -409,12 +411,12 @@ protected:
|
|||||||
int Readable(void) { return (tail >= head) ? size - tail - (head ? 0 : 1) : head - tail - 1; } // keep a 1 byte gap!
|
int Readable(void) { return (tail >= head) ? size - tail - (head ? 0 : 1) : head - tail - 1; } // keep a 1 byte gap!
|
||||||
int Writeable(void) { return (tail >= head) ? tail - head : size - head; }
|
int Writeable(void) { return (tail >= head) ? tail - head : size - head; }
|
||||||
int Byte(int Offset);
|
int Byte(int Offset);
|
||||||
void Set(int Offset, int Length, int Value);
|
bool Set(int Offset, int Length, int Value);
|
||||||
protected:
|
protected:
|
||||||
int GetStartCode(int Offset) { return (Byte(Offset) == 0x00 && Byte(Offset + 1) == 0x00 && Byte(Offset + 2) == 0x01) ? Byte(Offset + 3) : -1; }
|
int GetStartCode(int Offset) { return (Byte(Offset) == 0x00 && Byte(Offset + 1) == 0x00 && Byte(Offset + 2) == 0x01) ? Byte(Offset + 3) : -1; }
|
||||||
int GetPictureType(int Offset) { return (Byte(Offset + 5) >> 3) & 0x07; }
|
int GetPictureType(int Offset) { return (Byte(Offset + 5) >> 3) & 0x07; }
|
||||||
int FindStartCode(uchar Code, int Offset = 0);
|
int FindStartCode(uchar Code, int Offset = 0);
|
||||||
int GetAudioPacketLength(int Offset = 0);
|
int GetPacketLength(int Offset = 0);
|
||||||
public:
|
public:
|
||||||
cRingBuffer(int *InFile, int *OutFile, int Size, int FreeLimit = 0, int AvailLimit = 0);
|
cRingBuffer(int *InFile, int *OutFile, int Size, int FreeLimit = 0, int AvailLimit = 0);
|
||||||
virtual ~cRingBuffer();
|
virtual ~cRingBuffer();
|
||||||
@ -459,7 +461,7 @@ int cRingBuffer::Byte(int Offset)
|
|||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
void cRingBuffer::Set(int Offset, int Length, int Value)
|
bool cRingBuffer::Set(int Offset, int Length, int Value)
|
||||||
{
|
{
|
||||||
if (buffer && Offset + Length <= Available() ) {
|
if (buffer && Offset + Length <= Available() ) {
|
||||||
Offset += head;
|
Offset += head;
|
||||||
@ -469,7 +471,9 @@ void cRingBuffer::Set(int Offset, int Length, int Value)
|
|||||||
buffer[Offset] = Value;
|
buffer[Offset] = Value;
|
||||||
Offset++;
|
Offset++;
|
||||||
}
|
}
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
void cRingBuffer::Skip(int n)
|
void cRingBuffer::Skip(int n)
|
||||||
@ -598,15 +602,15 @@ int cRingBuffer::FindStartCode(uchar Code, int Offset)
|
|||||||
int c = GetStartCode(Offset + i);
|
int c = GetStartCode(Offset + i);
|
||||||
if (c == Code)
|
if (c == Code)
|
||||||
return i;
|
return i;
|
||||||
if (i > 0 && c == SC_BLOCK)
|
if (i > 0 && c == SC_PHEAD)
|
||||||
break; // found another block start while looking for a different code
|
break; // found another block start while looking for a different code
|
||||||
}
|
}
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
int cRingBuffer::GetAudioPacketLength(int Offset)
|
int cRingBuffer::GetPacketLength(int Offset)
|
||||||
{
|
{
|
||||||
// Returns the entire length of the audio packet starting at offset.
|
// Returns the entire length of the packet starting at offset.
|
||||||
return (Byte(Offset + 4) << 8) + Byte(Offset + 5) + 6;
|
return (Byte(Offset + 4) << 8) + Byte(Offset + 5) + 6;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -729,6 +733,7 @@ private:
|
|||||||
bool ok, synced, stop;
|
bool ok, synced, stop;
|
||||||
time_t lastDiskSpaceCheck;
|
time_t lastDiskSpaceCheck;
|
||||||
bool RunningLowOnDiskSpace(void);
|
bool RunningLowOnDiskSpace(void);
|
||||||
|
int ScanVideoPacket(int *PictureType, int Offset);
|
||||||
int Synchronize(void);
|
int Synchronize(void);
|
||||||
bool NextFile(void);
|
bool NextFile(void);
|
||||||
virtual int Write(int Max = -1);
|
virtual int Write(int Max = -1);
|
||||||
@ -807,40 +812,81 @@ bool cRecordBuffer::RunningLowOnDiskSpace(void)
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int cRecordBuffer::ScanVideoPacket(int *PictureType, int Offset)
|
||||||
|
{
|
||||||
|
// Scans the video packet starting at Offset and returns its length.
|
||||||
|
// If the return value is -1 the packet was not completely in the buffer.
|
||||||
|
|
||||||
|
int Length = GetPacketLength(Offset);
|
||||||
|
if (Length <= Available()) {
|
||||||
|
for (int i = Offset; i < Offset + Length; i++) {
|
||||||
|
if (Byte(i) == 0 && Byte(i + 1) == 0 && Byte(i + 2) == 1) {
|
||||||
|
switch (Byte(i + 3)) {
|
||||||
|
case SC_PICTURE: *PictureType = GetPictureType(i);
|
||||||
|
return Length;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*PictureType = NO_PICTURE;
|
||||||
|
return Length;
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
int cRecordBuffer::Synchronize(void)
|
int cRecordBuffer::Synchronize(void)
|
||||||
{
|
{
|
||||||
// Positions to the start of a data block (skipping everything up to
|
// Positions to the start of a data block (skipping everything up to
|
||||||
// an I-frame if not synced) and returns the block length.
|
// an I-frame if not synced) and returns the block length.
|
||||||
|
|
||||||
|
int LastPackHeader = -1;
|
||||||
|
|
||||||
pictureType = NO_PICTURE;
|
pictureType = NO_PICTURE;
|
||||||
bool Block = false;
|
|
||||||
for (int i = 0; Available() > MINVIDEODATA && i < MINVIDEODATA; i++) {
|
for (int i = 0; Available() > MINVIDEODATA && i < MINVIDEODATA; i++) {
|
||||||
if (Byte(i) == 0 && Byte(i + 1) == 0 && Byte(i + 2) == 1) {
|
if (Byte(i) == 0 && Byte(i + 1) == 0 && Byte(i + 2) == 1) {
|
||||||
switch (Byte(i + 3)) {
|
switch (Byte(i + 3)) {
|
||||||
case SC_BLOCK: if (Block && synced)
|
case SC_PHEAD: LastPackHeader = i;
|
||||||
return i; // found a block, so return its length
|
|
||||||
if (i) {
|
|
||||||
Skip(i);
|
|
||||||
if (synced)
|
|
||||||
esyslog(LOG_ERR, "ERROR: skipped %d bytes", i);
|
|
||||||
i = 0;
|
|
||||||
}
|
|
||||||
Block = true;
|
|
||||||
break;
|
break;
|
||||||
case SC_PICTURE: if (Block) {
|
case SC_VIDEO: {
|
||||||
pictureType = GetPictureType(i);
|
int pt = NO_PICTURE;
|
||||||
switch (pictureType) {
|
int l = ScanVideoPacket(&pt, i);
|
||||||
case I_FRAME: synced = true;
|
if (l < 0)
|
||||||
case P_FRAME:
|
return 0; // no useful data found, wait for more
|
||||||
case B_FRAME: break;
|
if (pt != NO_PICTURE) {
|
||||||
default: esyslog(LOG_ERR, "ERROR: unknown picture type '%d'", pictureType);
|
if (pt < I_FRAME || B_FRAME < pt) {
|
||||||
pictureType = NO_PICTURE;
|
esyslog(LOG_ERR, "ERROR: unknown picture type '%d'", pt);
|
||||||
|
}
|
||||||
|
else if (pictureType == NO_PICTURE) {
|
||||||
|
if (!synced) {
|
||||||
|
if (LastPackHeader == 0) {
|
||||||
|
if (pt == I_FRAME)
|
||||||
|
synced = true;
|
||||||
|
}
|
||||||
|
else if (LastPackHeader > 0) {
|
||||||
|
Skip(LastPackHeader);
|
||||||
|
LastPackHeader = -1;
|
||||||
|
i = 0;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
else { // LastPackHeader < 0
|
||||||
|
Skip(i + l);
|
||||||
|
i = 0;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (synced)
|
||||||
|
pictureType = pt;
|
||||||
|
}
|
||||||
|
else if (LastPackHeader > 0)
|
||||||
|
return LastPackHeader;
|
||||||
|
else
|
||||||
|
return i;
|
||||||
}
|
}
|
||||||
}
|
i += l - 1; // -1 to compensate for i++ in the loop!
|
||||||
else
|
LastPackHeader = -1;
|
||||||
esyslog(LOG_ERR, "ERROR: picture header outside of block");
|
}
|
||||||
break;
|
break;
|
||||||
case SC_AUDIO: i += GetAudioPacketLength(i) - 1; // -1 to compensate for i++ in the loop!
|
case SC_AUDIO: i += GetPacketLength(i) - 1; // -1 to compensate for i++ in the loop!
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -919,12 +965,13 @@ private:
|
|||||||
int replayFile;
|
int replayFile;
|
||||||
eReplayMode mode;
|
eReplayMode mode;
|
||||||
int lastIndex, stillIndex;
|
int lastIndex, stillIndex;
|
||||||
int brakeCounter, stillCounter;
|
int brakeCounter;
|
||||||
eReplayCmd command;
|
eReplayCmd command;
|
||||||
bool active;
|
bool active;
|
||||||
bool NextFile(uchar FileNumber = 0, int FileOffset = -1);
|
bool NextFile(uchar FileNumber = 0, int FileOffset = -1);
|
||||||
void Close(void);
|
void Close(void);
|
||||||
void SetCmd(eReplayCmd Cmd) { LOCK_THREAD; command = Cmd; }
|
void SetCmd(eReplayCmd Cmd) { LOCK_THREAD; command = Cmd; }
|
||||||
|
void SetTemporalReference(void);
|
||||||
protected:
|
protected:
|
||||||
virtual void Action(void);
|
virtual void Action(void);
|
||||||
public:
|
public:
|
||||||
@ -941,7 +988,7 @@ public:
|
|||||||
void Backward(void) { SetCmd(rcBackward); }
|
void Backward(void) { SetCmd(rcBackward); }
|
||||||
int SkipFrames(int Frames);
|
int SkipFrames(int Frames);
|
||||||
void SkipSeconds(int Seconds);
|
void SkipSeconds(int Seconds);
|
||||||
void Goto(int Position);
|
void Goto(int Position, bool Still = false);
|
||||||
void GetIndex(int &Current, int &Total, bool SnapToIFrame = false);
|
void GetIndex(int &Current, int &Total, bool SnapToIFrame = false);
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -954,7 +1001,7 @@ cReplayBuffer::cReplayBuffer(int *OutFile, const char *FileName)
|
|||||||
videoDev = *OutFile;
|
videoDev = *OutFile;
|
||||||
replayFile = fileName.Open();
|
replayFile = fileName.Open();
|
||||||
mode = rmPlay;
|
mode = rmPlay;
|
||||||
brakeCounter = stillCounter = 0;
|
brakeCounter = 0;
|
||||||
command = rcNone;
|
command = rcNone;
|
||||||
lastIndex = stillIndex = -1;
|
lastIndex = stillIndex = -1;
|
||||||
active = false;
|
active = false;
|
||||||
@ -1154,18 +1201,18 @@ void cReplayBuffer::SkipSeconds(int Seconds)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void cReplayBuffer::Goto(int Index)
|
void cReplayBuffer::Goto(int Index, bool Still)
|
||||||
{
|
{
|
||||||
LOCK_THREAD;
|
LOCK_THREAD;
|
||||||
|
|
||||||
command = rcStill;
|
if (Still)
|
||||||
|
command = rcStill;
|
||||||
if (++Index <= 0)
|
if (++Index <= 0)
|
||||||
Index = 1; // not '0', to allow GetNextIFrame() below to work!
|
Index = 1; // not '0', to allow GetNextIFrame() below to work!
|
||||||
uchar FileNumber;
|
uchar FileNumber;
|
||||||
int FileOffset;
|
int FileOffset;
|
||||||
if ((stillIndex = index->GetNextIFrame(Index, false, &FileNumber, &FileOffset)) >= 0)
|
if ((stillIndex = index->GetNextIFrame(Index, false, &FileNumber, &FileOffset)) >= 0)
|
||||||
NextFile(FileNumber, FileOffset);
|
NextFile(FileNumber, FileOffset);
|
||||||
stillCounter = 20; // apparently we need to repeat the still frame several times to flush all buffers?!
|
|
||||||
SetPlayMode(videoDev, VID_PLAY_CLEAR_BUFFER);
|
SetPlayMode(videoDev, VID_PLAY_CLEAR_BUFFER);
|
||||||
Clear();
|
Clear();
|
||||||
}
|
}
|
||||||
@ -1206,6 +1253,23 @@ bool cReplayBuffer::NextFile(uchar FileNumber, int FileOffset)
|
|||||||
return replayFile >= 0;
|
return replayFile >= 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void cReplayBuffer::SetTemporalReference(void)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < Available(); i++) {
|
||||||
|
if (Byte(i) == 0 && Byte(i + 1) == 0 && Byte(i + 2) == 1) {
|
||||||
|
switch (Byte(i + 3)) {
|
||||||
|
case SC_PICTURE: {
|
||||||
|
unsigned short m = (Byte(i + 4) << 8) | Byte(i + 5);
|
||||||
|
m &= 0x003F;
|
||||||
|
Set(i + 4, 1, m >> 8);
|
||||||
|
Set(i + 5, 1, m & 0xFF);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
int cReplayBuffer::Read(int Max = -1)
|
int cReplayBuffer::Read(int Max = -1)
|
||||||
{
|
{
|
||||||
if (mode != rmPlay) {
|
if (mode != rmPlay) {
|
||||||
@ -1213,18 +1277,14 @@ int cReplayBuffer::Read(int Max = -1)
|
|||||||
if (Available())
|
if (Available())
|
||||||
return 0; // write out the entire block
|
return 0; // write out the entire block
|
||||||
if (mode == rmStill) {
|
if (mode == rmStill) {
|
||||||
if (stillCounter > 0) {
|
uchar FileNumber;
|
||||||
stillCounter--;
|
int FileOffset, Length;
|
||||||
uchar FileNumber;
|
if (index->GetNextIFrame(stillIndex + 1, false, &FileNumber, &FileOffset, &Length) >= 0) {
|
||||||
int FileOffset, Length;
|
if (!NextFile(FileNumber, FileOffset))
|
||||||
if (index->GetNextIFrame(stillIndex + 1, false, &FileNumber, &FileOffset, &Length) >= 0) {
|
return -1;
|
||||||
if (!NextFile(FileNumber, FileOffset))
|
Max = Length;
|
||||||
return -1;
|
|
||||||
Max = Length;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
else
|
command = rcPause;
|
||||||
command = rcPause;
|
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
int Index = (lastIndex >= 0) ? lastIndex : index->Get(fileName.Number(), fileOffset);
|
int Index = (lastIndex >= 0) ? lastIndex : index->Get(fileName.Number(), fileOffset);
|
||||||
@ -1268,9 +1328,13 @@ int cReplayBuffer::Read(int Max = -1)
|
|||||||
} while (readin < Max && Free() > 0);
|
} while (readin < Max && Free() > 0);
|
||||||
if (mode != rmPlay) {
|
if (mode != rmPlay) {
|
||||||
// delete the audio data in modes other than rmPlay:
|
// delete the audio data in modes other than rmPlay:
|
||||||
int AudioOffset = FindStartCode(SC_AUDIO);
|
int AudioOffset, StartOffset = 0;
|
||||||
if (AudioOffset >= 0)
|
while ((AudioOffset = FindStartCode(SC_AUDIO, StartOffset)) >= 0) {
|
||||||
Set(AudioOffset, GetAudioPacketLength(AudioOffset), 0);
|
if (!Set(StartOffset + AudioOffset, GetPacketLength(StartOffset + AudioOffset), 0))
|
||||||
|
break; // to be able to replay old AV_PES recordings!
|
||||||
|
StartOffset += AudioOffset;
|
||||||
|
}
|
||||||
|
SetTemporalReference();
|
||||||
}
|
}
|
||||||
return readin;
|
return readin;
|
||||||
}
|
}
|
||||||
@ -2319,10 +2383,10 @@ bool cDvbApi::GetIndex(int &Current, int &Total, bool SnapToIFrame)
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
void cDvbApi::Goto(int Position)
|
void cDvbApi::Goto(int Position, bool Still)
|
||||||
{
|
{
|
||||||
if (replayBuffer)
|
if (replayBuffer)
|
||||||
replayBuffer->Goto(Position);
|
replayBuffer->Goto(Position, Still);
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- cEITScanner -----------------------------------------------------------
|
// --- cEITScanner -----------------------------------------------------------
|
||||||
|
7
dvbapi.h
7
dvbapi.h
@ -4,7 +4,7 @@
|
|||||||
* See the main source file 'vdr.c' for copyright information and
|
* See the main source file 'vdr.c' for copyright information and
|
||||||
* how to reach the author.
|
* how to reach the author.
|
||||||
*
|
*
|
||||||
* $Id: dvbapi.h 1.29 2000/12/25 15:17:03 kls Exp $
|
* $Id: dvbapi.h 1.30 2001/01/07 15:56:10 kls Exp $
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#ifndef __DVBAPI_H
|
#ifndef __DVBAPI_H
|
||||||
@ -239,8 +239,9 @@ public:
|
|||||||
bool GetIndex(int &Current, int &Total, bool SnapToIFrame = false);
|
bool GetIndex(int &Current, int &Total, bool SnapToIFrame = false);
|
||||||
// Returns the current and total frame index, optionally snapped to the
|
// Returns the current and total frame index, optionally snapped to the
|
||||||
// nearest I-frame.
|
// nearest I-frame.
|
||||||
void Goto(int Index);
|
void Goto(int Index, bool Still = false);
|
||||||
// Positions to the given index and displays that frame as a still picture.
|
// Positions to the given index and displays that frame as a still picture
|
||||||
|
// if Still is true.
|
||||||
};
|
};
|
||||||
|
|
||||||
class cEITScanner {
|
class cEITScanner {
|
||||||
|
6
menu.c
6
menu.c
@ -4,7 +4,7 @@
|
|||||||
* See the main source file 'vdr.c' for copyright information and
|
* See the main source file 'vdr.c' for copyright information and
|
||||||
* how to reach the author.
|
* how to reach the author.
|
||||||
*
|
*
|
||||||
* $Id: menu.c 1.56 2000/12/25 15:18:32 kls Exp $
|
* $Id: menu.c 1.57 2001/01/07 15:59:56 kls Exp $
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#include "menu.h"
|
#include "menu.h"
|
||||||
@ -2155,7 +2155,7 @@ void cReplayControl::MarkJump(bool Forward)
|
|||||||
if (dvbApi->GetIndex(Current, Total)) {
|
if (dvbApi->GetIndex(Current, Total)) {
|
||||||
cMark *m = Forward ? marks.GetNext(Current) : marks.GetPrev(Current);
|
cMark *m = Forward ? marks.GetNext(Current) : marks.GetPrev(Current);
|
||||||
if (m)
|
if (m)
|
||||||
dvbApi->Goto(m->position);
|
dvbApi->Goto(m->position, true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2175,7 +2175,7 @@ void cReplayControl::MarkMove(bool Forward)
|
|||||||
if ((m2 = marks.Prev(m)) != NULL && m2->position >= p)
|
if ((m2 = marks.Prev(m)) != NULL && m2->position >= p)
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
dvbApi->Goto(m->position = p);
|
dvbApi->Goto(m->position = p, true);
|
||||||
marks.Save();
|
marks.Save();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user