播放器开发(四):多线程解复用与解码模块实现

学习课题:逐步构建开发播放器【QT5 + FFmpeg6 + SDL2】

前言

根据第一章内容,我们首先可以先把解复用和解码模块完成,其中需要使用到多线程以及队列,还需要使用FFmpeg进行解复用和解码动作的实现。

创建BaseQueue基类

BaseQueue.h

#include <condition_variable>
#include <mutex>
#include <queue>

using namespace std;

template<class T>
class BaseQueue {
public:
    /**
     * 唤醒所有等待线程,设置异常标识为1
     */
    void abort() {
        m_abort = 1;
        m_cond.notify_all();
    }
    /**
     * push进m_queue
     *
     * @param val 需要push的值
     * @return  1是成功 or -1是m_abort==1可能有异常
     */
    int push(T val) {
        lock_guard<mutex> lock(m_mutex);
        if (m_abort == 1) {
            return -1;
        }
        m_queue.push(val);
        m_cond.notify_one();
        return 0;
    }
    /**
     * 从基类 front 到 val 并执行基类std::queue.pop
     *
     * @param val 存放front值的地址引用
     * @param timeout 持有锁的上限时间(ms)
     * @return 1是成功 or -1是m_abort==1可能有异常 or 是m_queue为空
     */
    int pop(T &val, int timeout = 0) {
        unique_lock<mutex> lock(m_mutex);
        if (m_queue.empty()) {
            m_cond.wait_for(lock, chrono::microseconds(timeout), [this] {
                return !m_queue.empty() | m_abort;
            });
        }
        if (m_abort == 1) {
            return -1;
        }
        if (m_queue.empty()) {
            return -2;
        }
        // there is address reference
        val = m_queue.front();
        m_queue.pop();
        return 0;
    }
    /**
     * 从基类std::queue.front 获取值保存到引用地址val
     *
     * @param val 存放front值的地址引用
     * @return 1是成功 or -1是m_abort==1可能有异常 or 是m_queue为空
     */
    int front(T &val) {
        lock_guard<mutex> lock(m_mutex);
        if (m_abort == 1) {
            return -1;
        }
        if (m_queue.empty()) {
            return -2;
        }
        val = m_queue.front();
        return 0;
    }
    int size() {
        lock_guard<mutex> lock(m_mutex);
        return m_queue.size();
    }

private:
    int m_abort = 0;// 是否中止
    mutex m_mutex;  // 锁
    condition_variable m_cond;
    queue<T> m_queue;
};

创建AVPacketQueue包队列类和AVFrameQueue帧队列类

在AVPacketQueue和AVFrameQueue中分别实现模版BaseQueue基类,并且可以添加一些错误的信息打印。


我们可以新建一个头文件FFmpegHeader.h用来存放导入ffmpeg的一些代码,后面用的时候导入这个文件就可以了

FFmpegHeader.h

extern "C" {
#include "libavcodec/avcodec.h"
#include "libavfilter/avfilter.h"
#include "libavformat/avformat.h"
#include "libavutil/audio_fifo.h"
#include "libavutil/avassert.h"
#include "libavutil/ffversion.h"
#include "libavutil/frame.h"
#include "libavutil/imgutils.h"
#include "libavutil/opt.h"
#include "libavutil/pixdesc.h"
#include "libavutil/time.h"
#include "libswresample/swresample.h"
#include "libswscale/swscale.h"

#ifdef ffmpegdevice
#include "libavdevice/avdevice.h"
#endif
}

#include "qdatetime.h"
#pragma execution_character_set("utf-8")

#define TIMEMS qPrintable(QTime::currentTime().toString("HH:mm:ss zzz"))
#define TIME qPrintable(QTime::currentTime().toString("HH:mm:ss"))
#define QDATE qPrintable(QDate::currentDate().toString("yyyy-MM-dd"))
#define QTIME qPrintable(QTime::currentTime().toString("HH-mm-ss"))
#define DATETIME qPrintable(QDateTime::currentDateTime().toString("yyyy-MM-dd HH:mm:ss"))
#define STRDATETIME qPrintable(QDateTime::currentDateTime().toString("yyyy-MM-dd-HH-mm-ss"))
#define STRDATETIMEMS qPrintable(QDateTime::currentDateTime().toString("yyyy-MM-dd-HH-mm-ss-zzz"))

AVPacketQueue

//AVPacketQueue.h
#include "BaseQueue.h"
#include "FFmpegHeader.h"
#include <cstdio>

class AVPacketQueue {
public:
    ~AVPacketQueue();
    int push(AVPacket *val);
    AVPacket *pop(int timeout);
    void release();
    int size();

private:
    BaseQueue<AVPacket *> m_queue;
};



//AVPacketQueue.cpp
#include "AVPacketQueue.h"

AVPacketQueue::~AVPacketQueue() {
    release();
    m_queue.abort();
}

int AVPacketQueue::push(AVPacket *val) {
    AVPacket *tmp_pkt = av_packet_alloc();
    av_packet_move_ref(tmp_pkt, val);
    return m_queue.push(tmp_pkt);
}
AVPacket *AVPacketQueue::pop(int timeout) {
    AVPacket *tmp_pkt = nullptr;
    int ret = m_queue.pop(tmp_pkt, timeout);
    if (ret == -1) {
        printf("AVPacketQueue::pop -->> m_abort==1可能有异常");
    }
    if (ret == -2) {
        printf("AVPacketQueue::pop -->> 队列为空");
    }
    return tmp_pkt;
}
void AVPacketQueue::release() {
    while (true) {
        AVPacket *pkt = nullptr;
        int ret = m_queue.pop(pkt, 1);
        if (ret < 0) {
            break;
        } else {
            av_packet_free(&pkt);
            continue;
        }
    }
}
int AVPacketQueue::size() {
    return m_queue.size();
}

AVFrameQueue

//AVFrameQueue.h
#include "BaseQueue.h"
#include "FFmpegHeader.h"
#include <cstdio>


class AVFrameQueue {
public:
    ~AVFrameQueue();
    int push(AVFrame *val);
    AVFrame *pop(int timeout);
    void release();
    int size();

private:
    BaseQueue<AVFrame *> m_queue;
};



//AVFrameQueue.cpp
#include "AVFrameQueue.h"
AVFrameQueue::~AVFrameQueue() {
    release();
    m_queue.abort();
}

int AVFrameQueue::push(AVFrame *val) {
    AVFrame *tmp_frame = av_frame_alloc();
    av_frame_move_ref(tmp_frame, val);
    return m_queue.push(tmp_frame);
}
AVFrame *AVFrameQueue::pop(int timeout) {
    AVFrame *tmp_frame = nullptr;
    int ret = m_queue.pop(tmp_frame, timeout);
    if (ret == -1) {
        printf("AVFrameQueue::pop -->> m_abort==1可能有异常");
    }
    if (ret == -2) {
        printf("AVFrameQueue::pop -->> 队列为空");
    }
    return tmp_frame;
}
void AVFrameQueue::release() {
    while (true) {
        AVFrame *pkt = nullptr;
        int ret = m_queue.pop(pkt, 1);
        if (ret < 0) {
            break;
        } else {
            av_frame_free(&pkt);
            continue;
        }
    }
}
int AVFrameQueue::size() {
    return m_queue.size();
}

创建BaseThread抽象类

#include <QThread>

/**
 * 因为我们后面可能需要用到qt信号传递是否暂停,所以使用QThread
 */
class BaseThread : public QThread {
    Q_OBJECT
public:
    // 初始化
    virtual int init() = 0;

    // 创建线程 开始run工作
    virtual int start() = 0;

    // 停止线程 释放资源
    virtual void stop() {
        isStopped = true;
        if (m_thread) {
            if (m_thread->isRunning()) {
                m_thread->wait(-1);
            }
            delete m_thread;
            m_thread = nullptr;
        }
    };

protected:
    bool isStopped = true; // 是否已经停止 停止时退出线程
    bool isPlaying = false;// 是否正在播放
    bool isPause = false;  // 是否暂停
    QThread *m_thread = nullptr;
};

创建DemuxThread解复用线程模块和DecodeThread解码线程模块

DemuxThread解复用线程:

1、打开视频文件,解封装操作。

2、读取流信息,并添加打印信息。

3、解复用(循环分离视频流和音频流)。

DemuxThread

//DemuxThread.h
enum class MediaType {
    Audio,
    Video
};

class DemuxThread : public BaseThread {
private:
    QString m_url = nullptr;
    AVFormatContext *ic = nullptr;
    int m_videoStreamIndex = -1;
    int m_audioStreamIndex = -1;
    const AVCodec *m_videoCodec;
    const AVCodec *m_audioCodec;
    AVPacketQueue *m_audioQueue;
    AVPacketQueue *m_videoQueue;

public:
    DemuxThread(AVPacketQueue *mAudioQueue, AVPacketQueue *mVideoQueue);
    DemuxThread(const QString &url, AVPacketQueue *mAudioQueue, AVPacketQueue *mVideoQueue);
    ~DemuxThread() override;
    // 打开视频文件,读取信息
    int init() override;
    int start() override;
    void stop() override;
    void run() override;

    void setUrl(const QString &url);


    AVCodecParameters *getCodecParameters(MediaType type);
    AVRational *getStreamTimeBase(MediaType type);
    const AVCodec *getCodec(MediaType type);
};



//DemuxThread.cpp
#include "DemuxThread.h"
DemuxThread::DemuxThread(AVPacketQueue *mAudioQueue, AVPacketQueue *mVideoQueue)
    : m_audioQueue(mAudioQueue), m_videoQueue(mVideoQueue) {
}
DemuxThread::DemuxThread(const QString &url, AVPacketQueue *mAudioQueue, AVPacketQueue *mVideoQueue)
    : m_url(url), m_audioQueue(mAudioQueue), m_videoQueue(mVideoQueue) {
}

DemuxThread::~DemuxThread() {
    if (m_thread) {
        this->stop();
    }
}
int DemuxThread::init() {
    if (m_url == nullptr) {
        qDebug() << "没有设置文件链接";
        return -1;
    }

    ic = avformat_alloc_context();
    int ret;
    ret = avformat_open_input(&ic, m_url.toUtf8(), nullptr, nullptr);
    if (ret < 0) {
        qDebug() << "avformat_open_input 函数发送错误";
        return -1;
    }

    ret = avformat_find_stream_info(ic, nullptr);
    if (ret < 0) {
        qDebug() << "avformat_find_stream_info 函数发送错误";
        return -1;
    }

    m_videoStreamIndex = av_find_best_stream(ic, AVMEDIA_TYPE_VIDEO, -1, -1, &m_videoCodec, 0);
    if (m_videoStreamIndex < 0) {
        qDebug() << "没有找到视频流索引 av_find_best_stream error";
        return -1;
    }
    AVCodecParameters *codecParameters_video = ic->streams[m_videoStreamIndex]->codecpar;
    QString codecNameVideo = avcodec_get_name(codecParameters_video->codec_id);
    qDebug() << "视频流:" << codecNameVideo;

    m_audioStreamIndex = av_find_best_stream(ic, AVMEDIA_TYPE_AUDIO, -1, -1, &m_audioCodec, 0);
    if (m_audioStreamIndex < 0) {
        qDebug() << "没有找到音频流索引 av_find_best_stream error";
        return -1;
    }
    AVCodecParameters *codecParameters_audio = ic->streams[m_audioStreamIndex]->codecpar;
    QString codecNameAudio = avcodec_get_name(codecParameters_audio->codec_id);
    qDebug() << "音频流:" << codecNameAudio;

    return 1;
}
int DemuxThread::start() {
    if (init() != 1) {
        qDebug() << "打开文件失败,停止创建线程";
        return -1;
    }
    isStopped = false;
    isPlaying = true;
    QThread::start();
    if (!currentThread()) {
        qDebug() << "线程创建失败";
        return -1;
    }

    return 0;
}
void DemuxThread::stop() {
    BaseThread::stop();
    if (ic) {
        avformat_close_input(&ic);
        ic = nullptr;
    }
    if (m_videoCodec) {
        m_videoCodec = nullptr;
    }

    if (m_audioCodec) {
        m_audioCodec = nullptr;
    }
}
void DemuxThread::run() {
    int ret;
    AVPacket pkt;
    while (!isStopped) {
        //
        if (m_audioQueue->size() > 10 || m_videoQueue->size() > 10) {
            //                        qDebug()<<"解复用线程等待 " <<"videoSize "<< m_videoQueue->size()<<"audioSize "<<m_audioQueue->size();
            msleep(10);
            //            std::this_thread::sleep_for(std::chrono::milliseconds(10));
            continue;
        }
        ret = av_read_frame(ic, &pkt);
        if (ret < 0) {
            qDebug() << "帧读完";
            break;
        }
        if (pkt.stream_index == m_audioStreamIndex) {
            m_audioQueue->push(&pkt);
            //            qDebug() << "audio pkt queue size:" << m_audioQueue->size();
        } else if (pkt.stream_index == m_videoStreamIndex) {
            m_videoQueue->push(&pkt);
            //            qDebug() << "video pkt queue size:" << m_videoQueue->size();
        } else {
            av_packet_unref(&pkt);
        }
    }
}

void DemuxThread::setUrl(const QString &url) {
    m_url = url;
}
AVCodecParameters *DemuxThread::getCodecParameters(MediaType type) {
    switch (type) {
        case MediaType::Audio:
            if (m_audioStreamIndex != -1) {
                return ic->streams[m_audioStreamIndex]->codecpar;
            } else {
                return nullptr;
            }
        case MediaType::Video:
            if (m_videoStreamIndex != -1) {
                return ic->streams[m_videoStreamIndex]->codecpar;
            } else {
                return nullptr;
            }
    }
}
AVRational *DemuxThread::getStreamTimeBase(MediaType type) {
    switch (type) {
        case MediaType::Audio:
            if (m_audioStreamIndex != -1) {
                return &ic->streams[m_audioStreamIndex]->time_base;
            } else {
                return nullptr;
            }
        case MediaType::Video:
            if (m_videoStreamIndex != -1) {
                return &ic->streams[m_videoStreamIndex]->time_base;
            } else {
                return nullptr;
            }
    }
}
const AVCodec *DemuxThread::getCodec(MediaType type) {
    switch (type) {
        case MediaType::Audio:
            return m_audioCodec;
        case MediaType::Video:
            return m_videoCodec;
    }
}

DecodeThread

//DecodeThread.h
#include "BaseThread.h"
#include "FFmpegHeader.h"
#include "queue/AVFrameQueue.h"
#include "queue/AVPacketQueue.h"
#include <QDebug>

class DecodeThread : public BaseThread {
private:
    const AVCodec *m_codec = nullptr;
    AVCodecParameters *m_par = nullptr;
    AVPacketQueue *m_packetQueue = nullptr;
    AVFrameQueue *m_frameQueue = nullptr;

public:
    AVCodecContext *dec_ctx = nullptr;

    DecodeThread(const AVCodec *mCodec, AVCodecParameters *mPar, AVPacketQueue *mPacketQueue, AVFrameQueue *mFrameQueue);
    ~DecodeThread() override;
    int init() override;
    int start() override;
    void stop() override;
    void run() override;
};



//DecodeThread.cpp
#include "DecodeThread.h"
DecodeThread::DecodeThread(const AVCodec *mCodec, AVCodecParameters *mPar, AVPacketQueue *mPacketQueue, AVFrameQueue *mFrameQueue)
    : m_codec(mCodec), m_par(mPar), m_packetQueue(mPacketQueue), m_frameQueue(mFrameQueue) {
}
DecodeThread::~DecodeThread() {
    stop();
}
int DecodeThread::init() {
    if (!m_par) {
        qDebug() << "AVCodecParameters 为空";
        return -1;
    }
    dec_ctx = avcodec_alloc_context3(nullptr);

    int ret = avcodec_parameters_to_context(dec_ctx, m_par);
    if (ret < 0) {
        qDebug() << "avcodec_parameters_to_context error";
    }

    ret = avcodec_open2(dec_ctx, m_codec, nullptr);
    if (ret < 0) {
        qDebug() << "avcodec_open2 error";
    }

    return 0;
}
void DecodeThread::run() {
    AVFrame *frame = av_frame_alloc();
    while (!isStopped) {
        if (m_frameQueue->size() > 10) {
            //                        qDebug()<<"解码线程等待";
            msleep(10);
            //            std::this_thread::sleep_for(std::chrono::milliseconds(10));
            continue;
        }

        AVPacket *pkt = m_packetQueue->pop(5);
        if (pkt) {
            int ret = avcodec_send_packet(dec_ctx, pkt);
            av_packet_free(&pkt);
            if (ret < 0) {
                qDebug() << "avcodec_send_packet error";
                break;
            }
            while (true) {
                ret = avcodec_receive_frame(dec_ctx, frame);
                if (ret == 0) {
                    m_frameQueue->push(frame);
                    //                    qDebug()<<"m_frameQueue size:"<<m_frameQueue->size();
                    continue;
                } else if (AVERROR(EAGAIN) == ret) {
                    break;
                } else {
                    isStopped = true;
                    qDebug() << "avcodec_receive_frame error";
                    break;
                }
            }
        } else {
            break;
        }
    }
}
int DecodeThread::start() {
    isStopped = false;
    isPlaying = true;
    QThread::start();
    if (!currentThread()) {
        qDebug() << "线程创建失败";
        return -1;
    }
    return 0;
}
void DecodeThread::stop() {
    BaseThread::stop();
    if (dec_ctx)
        avcodec_close(dec_ctx);
}

测试是否能够正常运行

现在解复用线程模块和解码线程模块都已经完成了,测试一下是否正常运行

main.cpp

#include <QApplication>
#include <QPushButton>
//-----------
#include "queue/AVFrameQueue.h"
#include "queue/AVPacketQueue.h"
#include "thread/DecodeThread.h"
#include "thread/DemuxThread.h"


int main(int argc, char *argv[]) {
    QApplication a(argc, argv);
    QPushButton button("Hello world!", nullptr);
    button.resize(200, 100);
    button.show();

    // 解复用
    DemuxThread *demuxThread;
    DecodeThread *audioDecodeThread;
    DecodeThread *videoDecodeThread;

    // 解码-音频
    AVPacketQueue audioPacketQueue;
    AVFrameQueue audioFrameQueue;
    // 解码-视频
    AVPacketQueue videoPacketQueue;
    AVFrameQueue videoFrameQueue;

    demuxThread = new DemuxThread(&audioPacketQueue, &videoPacketQueue);
    demuxThread->setUrl("/Users/mac/Downloads/23.mp4");
    demuxThread->start();
    int ret;
    audioDecodeThread = new DecodeThread(
            demuxThread->getCodec(MediaType::Audio),
            demuxThread->getCodecParameters(MediaType::Audio),
            &audioPacketQueue,
            &audioFrameQueue);
    audioDecodeThread->init();
    audioDecodeThread->start();

    videoDecodeThread = new DecodeThread(
            demuxThread->getCodec(MediaType::Video),
            demuxThread->getCodecParameters(MediaType::Video),
            &videoPacketQueue,
            &videoFrameQueue);
    videoDecodeThread->init();
    videoDecodeThread->start();

    return QApplication::exec();
}

测试完成运行正常

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/186073.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

BUUCTF 谁赢了比赛? 1

BUUCTF:https://buuoj.cn/challenges 题目描述&#xff1a; 小光非常喜欢下围棋。一天&#xff0c;他找到了一张棋谱&#xff0c;但是看不出到底是谁赢了。你能帮他看看到底是谁赢了么&#xff1f; 注意&#xff1a;得到的 flag 请包上 flag{} 提交 密文&#xff1a; 下载附…

代码随想录算法训练营第五十四天|392.判断子序列 115.不同的子序列

文档讲解&#xff1a;代码随想录 视频讲解&#xff1a;代码随想录B站账号 状态&#xff1a;看了视频题解和文章解析后做出来了 392.判断子序列 class Solution:def isSubsequence(self, s: str, t: str) -> bool:dp [[0] * (len(t)1) for _ in range(len(s)1)]for i in ra…

ky10 server x86 auditd安装(日志审计系统)

概述 Auditd工具可以帮助运维人员审计Linux&#xff0c;分析发生在系统中的发生的事情。Linux 内核有用日志记录事件的能力&#xff0c;包括记录系统调用和文件访问。管理员可以检查这些日志&#xff0c;确定是否存在安全漏洞&#xff08;如多次失败的登录尝试&#xff0c;或者…

2、分布式锁实现原理与最佳实践(二)

常见分布式锁的原理 4.1 Redisson Redis 2.6之后才可以执行lua脚本&#xff0c;比起管道而言&#xff0c;这是原子性的&#xff0c;模拟一个商品减库存的原子操作&#xff1a; //lua脚本命令执行方式&#xff1a;redis-cli --eval /tmp/test.lua , 10 jedis.set("produ…

【Redis】事务

文章目录 事务的概念事务操作MULTIEXECDISCARDWATCHUNWATCH 事务的概念 Redis 的事务和 MySQL 的事务概念上是类似的. 都是把⼀系列操作绑定成⼀组. 让这⼀组能够批量执 ⾏ Redis 的事务和 MySQL 事务的区别 弱化的原⼦性: redis 没有 “回滚机制”. 只能做到这些操作 “批量执…

基于 STM32F7 和神经网络的实时人脸特征提取与匹配算法实现

本文讨论了如何使用 STM32F7 和神经网络模型来实现实时人脸特征提取与匹配算法。首先介绍了 STM32F7 的硬件和软件特点&#xff0c;然后讨论了人脸特征提取和匹配算法的基本原理。接下来&#xff0c;我们将重点讨论如何在 STM32F7 上实现基于神经网络的人脸特征提取与匹配算法&…

2023年亚太杯数学建模A题解题思路(*基于OpenCV的复杂背景下苹果目标的识别定位方法研究)

摘要 由于要求较高的时效性和劳力投入&#xff0c;果实采摘环节成为苹果生产作业中十分重要的一部分。而对于自然环境下生长的苹果&#xff0c;光照影响、枝叶遮挡和果实重叠等情况普遍存在&#xff0c;这严重影响了果实的准确识别以及采摘点的精确定位。针对在复杂背景下苹果的…

Spring - Mybatis-设计模式总结

Mybatis-设计模式总结 1、Builder模式 2、工厂模式 3、单例模式 4、代理模式 5、组合模式 6、模板方法模式 7、适配器模式 8、装饰者模式 9、迭代器模式 虽然我们都知道有26个设计模式&#xff0c;但是大多停留在概念层面&#xff0c;真实开发中很少遇到&#xff0c;…

【迅搜03】全文检索、文档、倒排索引与分词

全文检索、文档、倒排索引与分词 今天还是概念性的内容&#xff0c;但是这些概念却是整个搜索引擎中最重要的概念。可以说&#xff0c;所有的搜索引擎就是实现了类似的概念才能称之为搜索引擎。而且今天的内容其实都是相关联的&#xff0c;所以不要以为标题上有四个名词就感觉好…

基于JavaWeb+SSM+Vue微信阅读小程序的设计和实现

基于JavaWebSSMVue微信阅读小程序的设计和实现 源码获取入口Lun文目录前言主要技术系统设计功能截图订阅经典源码专栏[Java 源码获取 源码获取入口 Lun文目录 第1章 绪论 1 1.1 课题背景 1 1.2 课题意义 1 1.3 研究内容 1 第2章 开发环境与技术 3 2.1 MYSQL数据库 3 2.2 JSP技…

微信小程序富文本拓展rich-text

微信小程序富文本插件 功能介绍 支持解析<style>标签中的全局样式支持自定义默认的标签样式支持自动设置标题 若html中存在title标签,将自动把title标签的内容设置到页面的标题上,并在回调bindparse中返回,可以用于转发支持添加加载提示 可以在Parser标签内添加加载提…

电机应用-直流有刷电机多环控制实现

目录 直流有刷电机多环控制实现 硬件设计 直流电机三环&#xff08;速度环、电流环、位置环&#xff09;串级PID控制-位置式PID 编程要点 配置ADC可读取电流值 配置基本定时器6产生定时中断读取当前电路中驱动电机的电流值并执行PID运算 配置定时器1输出PWM控制电机 配…

jenkins + gitlab 自动部署(webhook)

Jenkins是一个流行的开源CI/CD工具&#xff0c;可以与Git等版本控制系统集成&#xff0c;实现自动构建、测试和部署。Webhook是一种机制&#xff0c;可以在Git仓库中设置&#xff0c;在代码提交或合并请求时触发Jenkins构建任务&#xff0c;以完成自动化部署。 实操 设备信息 …

DELL MD3600F存储重置管理软件密码

注意&#xff1a;密码清除可能会导致业务秒断&#xff0c;建议非业务时间操作 针对一台控制器操作即可&#xff0c;另一控制器会同步操作 重置后密码为空&#xff01; 需求&#xff1a;重置存储管理软件密码 管理软件中分配物理磁盘时提示输入密码(类似是否了解风险确认操作的提…

前端(HTML + CSS + JS)

文章目录 一、HTML1. 概念&#xff08;1&#xff09;HTML 文件基本结构&#xff08;2&#xff09;HTML代码框架 2. 、HTML常见标签 二、CSS1. CSS基本语法规范2. 用法&#xff08;1&#xff09; 引用方式&#xff08;2&#xff09;选择器&#xff08;3&#xff09;常用元素属性…

面向对象三大特性,类与接口,java重写与重载,对象相等的判断, hashCode 与 equals

文章目录 2.1 面向对象三大特性2.1.1 封装 继承 多态2.1.2 其中Java 面向对象编程三大特性&#xff1a;封装 继承 多态2.1.3 关于继承如下 3 点请记住&#xff1a;2.1.4 什么是多态机制&#xff1f;Java语言是如何实现多态的&#xff1f;2.1.5 Java实现多态有三个必要条件&…

H5ke12--2--学生选课表格的编辑

方法1不可以修改的用label,如何按了哪一行 就会在下面有个文本显示可编辑的一行 方法2每一行后面都有一个编辑, 3对每一个修改,每一个td失去焦点都会有,直接到达我们服务器 注意 如果用span的每一个html元素都可以自己定义属性 Data-属性名,data-Address links也要给为span 1…

Qt学习(2)

1.QObject 只有继承了QObject类的类&#xff0c;才具有信号槽的能力。所以&#xff0c;为了使用信号槽&#xff0c;必须继承QObject。凡是QObject类&#xff08;不管是直接子类还是间接子类&#xff09;&#xff0c;都应该在第一行代码写上Q_OBJECT。不管是不是使用信号槽&…

【LeetCode】挑战100天 Day14(热题+面试经典150题)

【LeetCode】挑战100天 Day14&#xff08;热题面试经典150题&#xff09; 一、LeetCode介绍二、LeetCode 热题 HOT 100-162.1 题目2.2 题解 三、面试经典 150 题-163.1 题目3.2 题解 一、LeetCode介绍 LeetCode是一个在线编程网站&#xff0c;提供各种算法和数据结构的题目&…

nohup 实现远程运行不关机操作

nohup 实现远程运行不宕机操作 python nohup 实现远程运行不宕机操作 - python教程网 远程运行最怕断电&#xff0c;训练了几个小时的数据说没就没&#xff0c;或者停止运行。 用nohup 记录代码的输出&#xff0c;还可以不受断电的影响。 方法 1. 用nohup 运行一个python文…